فهرست منبع

app/wallet: wire shapes

x 1 هفته پیش
والد
کامیت
63417cb5c4

+ 6 - 5
bin/app/src/app/node.rs

@@ -1566,11 +1566,12 @@ pub fn create_tokentable(name: &str) -> SceneNode {
     prop.allow_exprs();
     node.add_property(prop).unwrap();
 
-    let mut prop =
-        Property::new("separator_color", PropertyType::Float32, PropertySubType::Color, PERM_STYLE);
-    prop.set_array_len(4);
-    prop.set_defaults_f32(vec![0.3, 0.3, 0.3, 1.]).unwrap();
-    prop.set_range_f32(0., 1.);
+    let mut prop = Property::new(
+        "separator_shape",
+        PropertyType::VectorShape,
+        PropertySubType::Null,
+        PERM_STYLE,
+    );
     prop.allow_exprs();
     node.add_property(prop).unwrap();
 

+ 10 - 25
bin/app/src/app/schema/wallet/main.rs

@@ -29,7 +29,7 @@ use crate::{
     prop::{PropertyAtomicGuard, PropertyBool, PropertyFloat32, Role},
     scene::{SceneNodePtr, Slot},
     shape, text,
-    theme::wire_color,
+    theme::{wire_color, wire_shape},
     ui::{Button, Layer, Text, TokenTable, VectorArt, VectorShape},
     util::i18n::I18nBabelFish,
 };
@@ -359,16 +359,9 @@ pub async fn make(
     prop.set_default_expr(2, code).unwrap();
     prop.set_default_f32(3, BUTTON_HEIGHT).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).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.,
-        COLOR_TEAL,
-    );
-    node.get_property("shape").unwrap().set_default_shape(0, shape).unwrap();
+    // Class wiring: the outline follows the shared button token.
+    let theme = app.sg_root.lookup_node("/theme").unwrap();
+    wire_shape(&node, "shape", &theme, "wallet.btn_shape").unwrap();
     let node =
         node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     main_layer.link(node);
@@ -466,16 +459,9 @@ pub async fn make(
     prop.set_default_expr(2, code).unwrap();
     prop.set_default_f32(3, BUTTON_HEIGHT).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).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.,
-        COLOR_TEAL,
-    );
-    node.get_property("shape").unwrap().set_default_shape(0, shape).unwrap();
+    // Class wiring: the outline follows the shared button token.
+    let theme = app.sg_root.lookup_node("/theme").unwrap();
+    wire_shape(&node, "shape", &theme, "wallet.btn_shape").unwrap();
     let node =
         node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     main_layer.link(node);
@@ -572,11 +558,10 @@ pub async fn make(
     tokens_table.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     tokens_table.set_property_u32(atom, Role::App, "priority", 0).unwrap();
 
-    // Class wiring: the token table follows the shared wallet text token
+    // Class wiring: the token table follows the shared wallet text token,
+    // and its row separators the shared separator shape token.
     wire_color(&tokens_table, "text_color", &theme, "wallet.text_color").unwrap();
-
-    let prop = tokens_table.get_property("separator_color").unwrap();
-    prop.set_default_f32_multi(&[0.2, 0.2745, 0.2784, 1.]).unwrap();
+    wire_shape(&tokens_table, "separator_shape", &theme, "separator_shape").unwrap();
 
     let tokens_table = tokens_table
         .setup(|me| TokenTable::new(me, app.renderer.clone(), app.redraw_trigger.clone()))

+ 4 - 11
bin/app/src/app/schema/wallet/receive.rs

@@ -26,7 +26,7 @@ use crate::{
     prop::{PropertyAtomicGuard, PropertyFloat32, PropertyStr, Role},
     scene::{SceneNodePtr, Slot},
     shape,
-    theme::wire_color,
+    theme::{wire_color, wire_shape},
     ui::{Button, Layer, Text, VectorArt, VectorShape},
     util::{clipboard, i18n::I18nBabelFish},
 };
@@ -122,16 +122,9 @@ pub async fn make(
     prop.set_default_expr(3, code).unwrap();
     prop.add_depend(Role::App, &addr_h_prop, 0, "addr_height");
     addr_bg_node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
-    let mut shape = VectorShape::new();
-    shape.add_filled_box(
-        expr::const_f32(0.),
-        expr::const_f32(0.),
-        expr::load_var("w"),
-        expr::load_var("h"),
-        [0.0353, 0.1294, 0.1333, 0.60],
-    );
-    let prop = addr_bg_node.get_property("shape").unwrap();
-    prop.set_default_shape(0, shape).unwrap();
+    // Class wiring: the box follows the shared address-box token.
+    let theme = app.sg_root.lookup_node("/theme").unwrap();
+    wire_shape(&addr_bg_node, "shape", &theme, "wallet.addr_box_shape").unwrap();
     let addr_bg_node = addr_bg_node
         .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
         .await;

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

@@ -108,10 +108,10 @@ pub async fn make(
             {
                 send_layer_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
             }
-            if let Some(send_layer_grey_node) = sg_root_for_built
-                .lookup_node("/window/content/wallet/send_step4_layer/send_btn_layer_grey")
+            if let Some(send_layer_inactive_node) = sg_root_for_built
+                .lookup_node("/window/content/wallet/send_step4_layer/send_btn_layer_inactive")
             {
-                send_layer_grey_node
+                send_layer_inactive_node
                     .set_property_bool(atom, Role::App, "is_visible", false)
                     .unwrap();
             }

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

@@ -29,7 +29,7 @@ use crate::{
     gfx::gfxtag,
     prop::{PropertyAtomicGuard, PropertyBool, PropertyFloat32, Role},
     scene::{SceneNodePtr, Slot},
-    theme::wire_color,
+    theme::{wire_color, wire_shape},
     ui::{Layer, TokenRow, TokenTable},
     util::i18n::I18nBabelFish,
 };
@@ -109,12 +109,11 @@ pub async fn make(
     send_tokens_table.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     send_tokens_table.set_property_u32(atom, Role::App, "priority", 0).unwrap();
 
-    // Class wiring: the token table follows the shared wallet text token
+    // Class wiring: the token table follows the shared wallet text token,
+    // and its row separators the shared separator shape token.
     let theme = app.sg_root.lookup_node("/theme").unwrap();
     wire_color(&send_tokens_table, "text_color", &theme, "wallet.text_color").unwrap();
-
-    let prop = send_tokens_table.get_property("separator_color").unwrap();
-    prop.set_default_f32_multi(&[0.2, 0.2745, 0.2784, 1.]).unwrap();
+    wire_shape(&send_tokens_table, "separator_shape", &theme, "separator_shape").unwrap();
 
     let send_tokens_table = send_tokens_table
         .setup(|me| TokenTable::new(me, app.renderer.clone(), app.redraw_trigger.clone()))

+ 6 - 4
bin/app/src/app/schema/wallet/send_step2.rs

@@ -277,7 +277,7 @@ pub async fn make(
     send_step2_layer.link(node);
 
     // Add recipient button
-    let (node, btn_layer_valid, btn_layer_grey) = create_bottom_button_with_states(
+    let (node, btn_layer_valid, btn_layer_inactive) = create_bottom_button_with_states(
         app,
         atom,
         &send_step2_layer,
@@ -295,15 +295,17 @@ pub async fn make(
     let recipient_text_sub = recipient_text.subscribe_modify();
     let redraw = app.redraw_trigger.clone();
     let btn_layer_valid_clone = btn_layer_valid.clone();
-    let btn_layer_grey_clone = btn_layer_grey.clone();
+    let btn_layer_inactive_clone = btn_layer_inactive.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 addr = recipient_input2.get_property_str("text").unwrap();
-            // Cyan for valid, grey for invalid: switch button state layers
+            // Cyan for valid, inactive 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();
+            btn_layer_inactive_clone
+                .set_property_bool(atom, Role::App, "is_visible", !valid)
+                .unwrap();
         }
     });
     app.tasks.lock().push(listen_recipient_text);

+ 12 - 19
bin/app/src/app/schema/wallet/send_step3.rs

@@ -33,7 +33,7 @@ use crate::{
     gfx::gfxtag,
     prop::{PropertyAtomicGuard, PropertyBool, PropertyFloat32, PropertyRect, Role},
     scene::{SceneNodePtr, Slot},
-    theme::wire_color,
+    theme::{wire_color, wire_shape},
     ui::{BaseEdit, BaseEditType, Layer, Text, VectorArt, VectorShape},
     util::i18n::I18nBabelFish,
 };
@@ -214,16 +214,9 @@ pub async fn make(
     prop.set_default_expr(3, code).unwrap();
     prop.add_depend(Role::App, &addr_h_prop, 0, "addr_height");
     addr_bg_node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
-    let mut shape = VectorShape::new();
-    shape.add_filled_box(
-        expr::const_f32(0.),
-        expr::const_f32(0.),
-        expr::load_var("w"),
-        expr::load_var("h"),
-        [0.0353, 0.1294, 0.1333, 0.60],
-    );
-    let prop = addr_bg_node.get_property("shape").unwrap();
-    prop.set_default_shape(0, shape).unwrap();
+    // Class wiring: the box follows the shared address-box token.
+    let theme = app.sg_root.lookup_node("/theme").unwrap();
+    wire_shape(&addr_bg_node, "shape", &theme, "wallet.addr_box_shape").unwrap();
     let addr_bg_node = addr_bg_node
         .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
         .await;
@@ -427,7 +420,7 @@ pub async fn make(
     amount_wrapper.link(token_symbol_node.clone());
 
     // Add amount button with states
-    let (node, btn_layer_valid, btn_layer_grey) = create_bottom_button_with_states(
+    let (node, btn_layer_valid, btn_layer_inactive) = create_bottom_button_with_states(
         app,
         atom,
         &send_step3_layer,
@@ -437,7 +430,7 @@ pub async fn make(
         "add amount",
         &window_scale,
         i18n_fish,
-        false, // Start with invalid state (grey)
+        false, // Start with invalid (inactive) state
     )
     .await;
 
@@ -452,7 +445,7 @@ pub async fn make(
     let redraw = app.redraw_trigger.clone();
     let sg_root = app.sg_root.clone();
     let btn_layer_valid_clone = btn_layer_valid.clone();
-    let btn_layer_grey_clone = btn_layer_grey.clone();
+    let btn_layer_inactive_clone = btn_layer_inactive.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"));
@@ -498,11 +491,11 @@ pub async fn make(
                     }
                 };
 
-                // Cyan for valid, grey for invalid: switch button state layers
+                // Cyan for valid, inactive 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
+                btn_layer_inactive_clone
                     .set_property_bool(atom, Role::App, "is_visible", !is_valid)
                     .unwrap();
             }
@@ -573,10 +566,10 @@ pub async fn make(
                 {
                     send_layer_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")
+                if let Some(send_layer_inactive_node) =
+                    sg_root.lookup_node("/window/content/wallet/send_step4_layer/send_btn_layer_inactive")
                 {
-                    send_layer_grey_node
+                    send_layer_inactive_node
                         .set_property_bool(atom, Role::App, "is_visible", true)
                         .unwrap();
                 }

+ 8 - 15
bin/app/src/app/schema/wallet/send_step4.rs

@@ -30,7 +30,7 @@ use crate::{
     gfx::gfxtag,
     prop::{PropertyAtomicGuard, PropertyBool, PropertyFloat32, Role},
     scene::{SceneNodePtr, Slot},
-    theme::wire_color,
+    theme::{wire_color, wire_shape},
     ui::{Layer, Text, VectorArt, VectorShape},
     util::i18n::I18nBabelFish,
 };
@@ -209,16 +209,9 @@ pub async fn make(
     prop.set_default_expr(3, code).unwrap();
     prop.add_depend(Role::App, &addr_h_prop, 0, "addr_height");
     addr_bg_node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
-    let mut shape = VectorShape::new();
-    shape.add_filled_box(
-        expr::const_f32(0.),
-        expr::const_f32(0.),
-        expr::load_var("w"),
-        expr::load_var("h"),
-        [0.0353, 0.1294, 0.1333, 0.60],
-    );
-    let prop = addr_bg_node.get_property("shape").unwrap();
-    prop.set_default_shape(0, shape).unwrap();
+    // Class wiring: the box follows the shared address-box token.
+    let theme = app.sg_root.lookup_node("/theme").unwrap();
+    wire_shape(&addr_bg_node, "shape", &theme, "wallet.addr_box_shape").unwrap();
     let addr_bg_node = addr_bg_node
         .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
         .await;
@@ -388,7 +381,7 @@ pub async fn make(
     .await;
 
     // Send button (bottom button)
-    let (node, send_btn_layer, _send_btn_layer_grey) = create_bottom_button_with_states(
+    let (node, send_btn_layer, _send_btn_layer_inactive) = create_bottom_button_with_states(
         app,
         atom,
         &send_step4_layer,
@@ -544,10 +537,10 @@ pub async fn make(
                         .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")
+                if let Some(send_layer_inactive_node) = sg_root
+                    .lookup_node("/window/content/wallet/send_step4_layer/send_btn_layer_inactive")
                 {
-                    send_layer_grey_node
+                    send_layer_inactive_node
                         .set_property_bool(atom, Role::App, "is_visible", true)
                         .unwrap();
                 }

+ 33 - 85
bin/app/src/app/schema/wallet/util.rs

@@ -34,7 +34,7 @@ use crate::{
     prop::{PropertyAtomicGuard, PropertyFloat32, Role},
     scene::{SceneNodePtr, Slot},
     text,
-    theme::wire_color,
+    theme::{wire_color, wire_shape},
     ui::{Button, Layer, RedrawTrigger, Shortcut, Text, VectorArt, VectorShape},
     util::i18n::I18nBabelFish,
 };
@@ -189,19 +189,10 @@ pub async fn create_title(
     prop.set_default_expr(2, expr::load_var("w")).unwrap();
     prop.set_default_f32(3, (TITLE_PADDING * 2. + TITLE_FONTSIZE) / 2.).unwrap();
     bg_node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
-    let mut shape = VectorShape::new();
-    shape.add_smooth_vertical_gradient(
-        expr::const_f32(0.),
-        expr::const_f32(0.),
-        expr::load_var("w"),
-        expr::load_var("h"),
-        [0., 0.9059, 0.9216, 0.20],
-        [0., 0.8784, 1., 0.],
-        8,
-        0.2,
-    );
-    let prop = bg_node.get_property("shape").unwrap();
-    prop.set_default_shape(0, shape).unwrap();
+    // Class wiring: the bg follows the shared title token, so themes
+    // restyle every title by writing the token once.
+    let theme = app.sg_root.lookup_node("/theme").unwrap();
+    wire_shape(&bg_node, "shape", &theme, "wallet.title_shape").unwrap();
     let bg_node = bg_node
         .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
         .await;
@@ -323,10 +314,6 @@ pub async fn create_separator_expr(
     y_expr: &str,
     dimmed: bool,
 ) -> SceneNodePtr {
-    let color = match dimmed {
-        true => [0.1333, 0.1725, 0.1725, 1.],
-        false => [0.2, 0.2745, 0.2784, 1.],
-    };
     let sep_node = create_vector_art(name);
     let prop = sep_node.get_property("rect").unwrap();
     prop.set_default_f32(0, 0.).unwrap();
@@ -335,16 +322,11 @@ pub async fn create_separator_expr(
     prop.set_default_expr(2, expr::load_var("w")).unwrap();
     prop.set_default_f32(3, 1.).unwrap();
     sep_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
-    let mut shape = VectorShape::new();
-    shape.add_filled_box(
-        expr::const_f32(0.),
-        expr::const_f32(0.),
-        expr::load_var("w"),
-        expr::const_f32(1.),
-        color,
-    );
-    let prop = sep_node.get_property("shape").unwrap();
-    prop.set_default_shape(0, shape).unwrap();
+    // Class wiring: the shape follows the shared separator token, so
+    // themes restyle every separator by writing the token once.
+    let theme = app.sg_root.lookup_node("/theme").unwrap();
+    let token = if dimmed { "separator_dim_shape" } else { "separator_shape" };
+    wire_shape(&sep_node, "shape", &theme, token).unwrap();
     let sep_node = sep_node
         .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
         .await;
@@ -362,10 +344,6 @@ pub async fn create_separator(
     y: &mut f32,
     dimmed: bool,
 ) -> SceneNodePtr {
-    let color = match dimmed {
-        true => [0.1333, 0.1725, 0.1725, 1.],
-        false => [0.2, 0.2745, 0.2784, 1.],
-    };
     let sep_node = create_vector_art(name);
     let prop = sep_node.get_property("rect").unwrap();
     prop.set_default_f32(0, 0.).unwrap();
@@ -373,16 +351,10 @@ pub async fn create_separator(
     prop.set_default_expr(2, expr::load_var("w")).unwrap();
     prop.set_default_f32(3, 1.).unwrap();
     sep_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
-    let mut shape = VectorShape::new();
-    shape.add_filled_box(
-        expr::const_f32(0.),
-        expr::const_f32(0.),
-        expr::load_var("w"),
-        expr::const_f32(1.),
-        color,
-    );
-    let prop = sep_node.get_property("shape").unwrap();
-    prop.set_default_shape(0, shape).unwrap();
+    // Class wiring: see `create_separator_expr`.
+    let theme = app.sg_root.lookup_node("/theme").unwrap();
+    let token = if dimmed { "separator_dim_shape" } else { "separator_shape" };
+    wire_shape(&sep_node, "shape", &theme, token).unwrap();
     let sep_node = sep_node
         .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
         .await;
@@ -430,17 +402,9 @@ pub async fn create_bottom_button(
     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();
-    shape.add_outline(
-        expr::const_f32(0.),
-        expr::const_f32(0.),
-        expr::load_var("w"),
-        expr::load_var("h"),
-        1.,
-        COLOR_TEAL,
-    );
-    let prop = node.get_property("shape").unwrap();
-    prop.set_default_shape(0, shape).unwrap();
+    // Class wiring: see `create_bottom_button_state`.
+    let theme = app.sg_root.lookup_node("/theme").unwrap();
+    wire_shape(&node, "shape", &theme, "wallet.btn_shape").unwrap();
     let node =
         node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     btn_layer.link(node);
@@ -492,9 +456,9 @@ pub async fn create_bottom_button(
 }
 
 /// Creates a bottom button with two states: valid (teal, active) and
-/// invalid (grey, inactive). Each state is a self-contained layer, so
+/// invalid (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).
+/// Returns (button_node, valid_layer, inactive_layer).
 pub async fn create_bottom_button_with_states(
     app: &App,
     atom: &mut PropertyAtomicGuard,
@@ -502,7 +466,7 @@ pub async fn create_bottom_button_with_states(
     name: &str,
     cc: &mut Compiler,
     label_valid: &str,
-    label_grey: &str,
+    label_inactive: &str,
     window_scale: &PropertyFloat32,
     i18n_fish: &I18nBabelFish,
     initial_valid: bool,
@@ -516,34 +480,32 @@ pub async fn create_bottom_button_with_states(
         label_valid,
         window_scale,
         i18n_fish,
-        COLOR_TEAL,
         COLOR_CYAN,
         true,
         initial_valid,
         "",
     )
     .await;
-    let grey_layer = create_bottom_button_state(
+    let inactive_layer = create_bottom_button_state(
         app,
         atom,
         layer,
         name,
         cc,
-        label_grey,
+        label_inactive,
         window_scale,
         i18n_fish,
         [0.5, 0.5, 0.5, 1.],
-        [0.5, 0.5, 0.5, 1.],
         false,
         !initial_valid,
-        "_grey",
+        "_inactive",
     )
     .await;
 
     let btn = btn_layer
         .lookup_node(&format!("/{name}"))
         .unwrap_or_else(|| panic!("bottom button node missing: {name}"));
-    (btn, btn_layer, grey_layer)
+    (btn, btn_layer, inactive_layer)
 }
 
 /// One state of a bottom button, wrapped in its own layer so callers can
@@ -559,7 +521,6 @@ async fn create_bottom_button_state(
     label_text: &str,
     window_scale: &PropertyFloat32,
     i18n_fish: &I18nBabelFish,
-    outline_color: Color,
     label_color: Color,
     is_active: bool,
     is_visible: bool,
@@ -589,17 +550,12 @@ async fn create_bottom_button_state(
     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.),
-        expr::const_f32(0.),
-        expr::load_var("w"),
-        expr::load_var("h"),
-        1.,
-        outline_color,
-    );
-    let prop = node.get_property("shape").unwrap();
-    prop.set_default_shape(0, shape).unwrap();
+    // Class wiring: the outline follows the shared button token, active
+    // and inactive states on separate tokens (state switch is a layer
+    // visibility toggle, so each state's bg stays on its own token).
+    let theme = app.sg_root.lookup_node("/theme").unwrap();
+    let token = if is_active { "wallet.btn_shape" } else { "wallet.btn_inactive_shape" };
+    wire_shape(&node, "shape", &theme, token).unwrap();
     let node =
         node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     btn_layer.link(node);
@@ -704,17 +660,9 @@ pub async fn create_tooltip(
     let code = cc.compile("TOOLTIP_HEIGHT").unwrap();
     prop.set_default_expr(3, code).unwrap();
 
-    let mut shape = VectorShape::new();
-    shape.add_outline(
-        expr::const_f32(0.),
-        expr::const_f32(0.),
-        expr::const_f32(width),
-        expr::const_f32(height),
-        1.,
-        text_color,
-    );
-    let prop = node.get_property("shape").unwrap();
-    prop.set_default_shape(0, shape).unwrap();
+    // Class wiring: same button-outline class as the other buttons.
+    let theme = app.sg_root.lookup_node("/theme").unwrap();
+    wire_shape(&node, "shape", &theme, "wallet.btn_shape").unwrap();
     let node =
         node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     tooltip_layer.link(node);

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

@@ -692,10 +692,10 @@ async fn load_plugins(
                         .set_property_bool(atom, Role::App, "is_visible", false)
                         .unwrap();
                 }
-                if let Some(send_layer_grey_node) = sg_root2
-                    .lookup_node("/window/content/wallet/send_step4_layer/send_btn_layer_grey")
+                if let Some(send_layer_inactive_node) = sg_root2
+                    .lookup_node("/window/content/wallet/send_step4_layer/send_btn_layer_inactive")
                 {
-                    send_layer_grey_node
+                    send_layer_inactive_node
                         .set_property_bool(atom, Role::App, "is_visible", true)
                         .unwrap();
                 }

+ 32 - 0
bin/app/src/shape/filled_box.rs

@@ -0,0 +1,32 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2026 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+use crate::{expr, mesh::Color, ui::VectorShape};
+
+pub fn create_filled_box(color: Color) -> VectorShape {
+    let mut shape = VectorShape::new();
+    shape.add_filled_box(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        color,
+    );
+    shape
+}

+ 32 - 0
bin/app/src/shape/h_line.rs

@@ -0,0 +1,32 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2026 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+use crate::{expr, mesh::Color, ui::VectorShape};
+
+pub fn create_h_line(color: Color) -> VectorShape {
+    let mut shape = VectorShape::new();
+    shape.add_filled_box(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::const_f32(1.),
+        color,
+    );
+    shape
+}

+ 9 - 0
bin/app/src/shape/mod.rs

@@ -110,3 +110,12 @@ pub use reset::create_reset;
 
 mod slider_thumb;
 pub use slider_thumb::create_thumb;
+
+mod h_line;
+pub use h_line::create_h_line;
+
+mod outline_box;
+pub use outline_box::create_outline_box;
+
+mod filled_box;
+pub use filled_box::create_filled_box;

+ 33 - 0
bin/app/src/shape/outline_box.rs

@@ -0,0 +1,33 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2026 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+use crate::{expr, mesh::Color, ui::VectorShape};
+
+pub fn create_outline_box(color: Color) -> VectorShape {
+    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.,
+        color,
+    );
+    shape
+}

+ 13 - 114
bin/app/src/theme/light.rs

@@ -108,6 +108,11 @@ async fn light_apply(ctx: &ThemeCtx) -> Result<()> {
     let prop = node.get_property("shape").unwrap();
     ctx.set_touched(atom, &prop, 0, PropertyValue::VectorShape(Arc::new(shape)))?;
 
+    // Separators
+    let sep_color = [0., 0.2745, 0.2784, 1.];
+    ctx.set_shared_token_shape(atom, "separator_dim_shape", shape::create_h_line(sep_color))?;
+    ctx.set_shared_token_shape(atom, "separator_shape", shape::create_h_line(sep_color))?;
+
     // Main balance background
     let node = sg_root.lookup_node("/window/content/wallet/main_layer/wallet_balance_bg").unwrap();
     let mut shape = VectorShape::new();
@@ -193,121 +198,15 @@ async fn light_apply(ctx: &ThemeCtx) -> Result<()> {
     let prop = node.get_property("shape").unwrap();
     ctx.set_touched(atom, &prop, 0, PropertyValue::VectorShape(Arc::new(shape)))?;
 
-    // Wallet receive/send button outlines
-    let outline_overrides = [
-        "/window/content/wallet/main_layer/receive_btn_bg",
-        "/window/content/wallet/main_layer/wallet_send_btn_bg",
-    ];
-    for path in outline_overrides {
-        let node = sg_root.lookup_node(path).unwrap();
-        let prop = node.get_property("shape").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.,
-            accent,
-        );
-        ctx.set_touched(atom, &prop, 0, PropertyValue::VectorShape(Arc::new(shape)))?;
-    }
-
-    // Address boxes
-    let addr_box_paths = [
-        "/window/content/wallet/receive_layer/receive_address_bg",
-        "/window/content/wallet/send_step3_layer/send_recipient_bg",
-        "/window/content/wallet/send_step4_layer/send_recipient_bg",
-    ];
-    for path in addr_box_paths {
-        let node = sg_root.lookup_node(path).unwrap();
-        let prop = node.get_property("shape").unwrap();
-        let mut shape = VectorShape::new();
-        shape.add_filled_box(
-            expr::const_f32(0.),
-            expr::const_f32(0.),
-            expr::load_var("w"),
-            expr::load_var("h"),
-            [0.88, 0.92, 0.99, 0.75],
-        );
-        ctx.set_touched(atom, &prop, 0, PropertyValue::VectorShape(Arc::new(shape)))?;
-    }
-
-    // Wallet bottom button outlines
-    let bottom_btn_outlines = [
-        "/window/content/wallet/receive_layer/receive_copy_tooltip/bg",
-        "/window/content/wallet/send_step2_layer/send_add_recipient_btn_layer/send_add_recipient_btn_bg",
-        "/window/content/wallet/send_step3_layer/send_add_amount_btn_layer/send_add_amount_btn_bg",
-        "/window/content/wallet/send_step4_layer/send_btn_layer/send_btn_bg",
-        "/window/content/wallet/tx_status_layer/close_btn_layer/close_btn_bg",
-    ];
-    for path in bottom_btn_outlines {
-        let node = sg_root.lookup_node(path).unwrap();
-        let prop = node.get_property("shape").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.,
-            accent,
-        );
-        ctx.set_touched(atom, &prop, 0, PropertyValue::VectorShape(Arc::new(shape)))?;
-    }
+    // Wallet buttons
+    ctx.set_shared_token_shape(atom, "wallet.btn_shape", shape::create_outline_box(accent))?;
 
-    // Wallet separators
-    let separator_color = [0.2, 0.2745, 0.2784, 1.];
-    let separator_paths = [
-        "/window/content/wallet/main_layer/wallet_balance_separator",
-        "/window/content/wallet/main_layer/wallet_buttons_separator",
-        "/window/content/wallet/main_layer/tokens_label_separator",
-        "/window/content/wallet/receive_layer/receive_separator",
-        "/window/content/wallet/receive_layer/address_separator",
-        "/window/content/wallet/send_step1_layer/send_separator",
-        "/window/content/wallet/send_step1_layer/pick_label_separator",
-        "/window/content/wallet/send_step2_layer/send_separator",
-        "/window/content/wallet/send_step2_layer/send_token_separator",
-        "/window/content/wallet/send_step3_layer/send_separator",
-        "/window/content/wallet/send_step3_layer/send_token_separator3",
-        "/window/content/wallet/send_step3_layer/send_amount_label_separator",
-        "/window/content/wallet/send_step3_layer/recipient_separator",
-        "/window/content/wallet/send_step3_layer/available_balance_separator",
-        "/window/content/wallet/send_step4_layer/send_separator",
-        "/window/content/wallet/send_step4_layer/send_token_separator4",
-        "/window/content/wallet/send_step4_layer/send_amount_label_separator4",
-        "/window/content/wallet/send_step4_layer/recipient_separator",
-        "/window/content/wallet/send_step4_layer/tx_fee_separator",
-        "/window/content/wallet/tx_status_layer/hint_separator",
-    ];
-    for path in separator_paths {
-        let node = sg_root.lookup_node(path).unwrap();
-        let prop = node.get_property("shape").unwrap();
-        let mut shape = VectorShape::new();
-        shape.add_filled_box(
-            expr::const_f32(0.),
-            expr::const_f32(0.),
-            expr::load_var("w"),
-            expr::const_f32(1.),
-            separator_color,
-        );
-        ctx.set_touched(atom, &prop, 0, PropertyValue::VectorShape(Arc::new(shape)))?;
-    }
-
-    // Wallet titles
-    let title_bg_paths = [
-        "/window/content/wallet/main_layer/tokens_label_bg",
-        "/window/content/wallet/receive_layer/receive_bg",
-        "/window/content/wallet/send_step1_layer/send_bg",
-        "/window/content/wallet/send_step2_layer/send_bg",
-        "/window/content/wallet/send_step3_layer/send_bg",
-        "/window/content/wallet/send_step4_layer/send_bg",
-    ];
-    for path in title_bg_paths {
-        let node = sg_root.lookup_node(path).unwrap();
-        let prop = node.get_property("shape").unwrap();
-        ctx.set_touched(atom, &prop, 0, PropertyValue::VectorShape(Arc::new(VectorShape::new())))?;
-    }
+    // Wallet address boxes
+    ctx.set_shared_token_shape(
+        atom,
+        "wallet.addr_box_shape",
+        shape::create_filled_box([0.88, 0.92, 0.99, 0.75]),
+    )?;
 
     // Chat labels
     let node = sg_root.lookup_node("/window/content/chat/main_chat_layer/channel_label").unwrap();

+ 10 - 0
bin/app/src/theme/mod.rs

@@ -36,12 +36,14 @@ use crate::{
     error::{Error, Result},
     expr,
     gfx::gfxtag,
+    mesh::rgba,
     prop::{
         Property, PropertyAtomicGuard, PropertyPermission, PropertyPtr, PropertySubType,
         PropertyType, PropertyValue, Role,
     },
     pubsub::Subscription,
     scene::{Pimpl, SceneNode, SceneNodePtr, SceneNodeType},
+    shape,
     ui::{get_ui_object3, get_ui_object_ptr, RedrawTrigger, VectorShape},
 };
 
@@ -87,6 +89,8 @@ pub fn shared_token_props() -> Vec<Property> {
     vec![
         // Generic surfaces
         color_prop("text_color", [0.92, 0.92, 0.92, 1.]),
+        shape_prop("separator_shape", shape::create_h_line([0.2, 0.2745, 0.2784, 1.])),
+        shape_prop("separator_dim_shape", shape::create_h_line([0.1333, 0.1725, 0.1725, 1.])),
         // Edit widget
         color_prop("edit.text_color", [0.92, 0.92, 0.92, 1.]),
         color_prop("edit.hi_bg_color", [0.35, 0.35, 0.35, 1.]),
@@ -105,6 +109,10 @@ pub fn shared_token_props() -> Vec<Property> {
         color_prop("chatview.action_text_color", [0.80, 0.80, 0.80, 1.]),
         color_prop("chatview.url_text_color", [0.70, 0.80, 0.90, 1.]),
         // Wallet
+        shape_prop("wallet.title_shape", VectorShape::new()),
+        shape_prop("wallet.addr_box_shape", shape::create_filled_box(rgba!(0x09212299))),
+        shape_prop("wallet.btn_shape", shape::create_outline_box(rgba!(0x14AEB8ff))),
+        shape_prop("wallet.btn_inactive_shape", shape::create_outline_box(rgba!(0x808080ff))),
         color_prop("wallet.text_color", [1., 1., 1., 1.]),
         color_prop("wallet.title_color", [0.6471, 0.8667, 0.8157, 1.]),
         color_prop("wallet.label_color", [0.6902, 1., 1., 1.]),
@@ -684,6 +692,8 @@ mod tests {
         let referenced = [
             // generic
             "text_color",
+            "separator_shape",
+            "separator_dim_shape",
             // edit
             "edit.text_color",
             "edit.hi_bg_color",

+ 14 - 0
bin/app/src/theme/scifi.rs

@@ -164,6 +164,20 @@ async fn scifi_apply(ctx: &ThemeCtx) -> Result<()> {
         }
     }
 
+    // Wallet title bg gradient
+    let mut title_bg = VectorShape::new();
+    title_bg.add_smooth_vertical_gradient(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        [0., 0.9059, 0.9216, 0.20],
+        [0., 0.8784, 1., 0.],
+        8,
+        0.2,
+    );
+    ctx.set_shared_token_shape(atom, "wallet.title_shape", title_bg)?;
+
     // Recipient input: add radial glow
     let node = ctx
         .sg_root()

+ 13 - 9
bin/app/src/ui/tokentable/mod.rs

@@ -28,7 +28,8 @@ use crate::{
     gfx::{gfxtag, DrawCall, DrawInstruction, EpochCache, Point, Rectangle, RenderApi, Renderer},
     mesh::MeshBuilder,
     prop::{
-        PropertyAtomicGuard, PropertyColor, PropertyFloat32, PropertyRect, PropertyUint32, Role,
+        PropertyAtomicGuard, PropertyColor, PropertyFloat32, PropertyRect, PropertyShape,
+        PropertyUint32, Role,
     },
     scene::SceneNodeWeak,
     text,
@@ -74,7 +75,7 @@ pub struct TokenTable {
 
     font_size: PropertyFloat32,
     text_color: PropertyColor,
-    separator_color: PropertyColor,
+    separator_shape: PropertyShape,
     padding_x: PropertyFloat32,
     padding_y: PropertyFloat32,
 
@@ -92,8 +93,8 @@ impl TokenTable {
         let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
         let font_size = PropertyFloat32::wrap(node_ref, Role::Internal, "font_size", 0).unwrap();
         let text_color = PropertyColor::wrap(node_ref, Role::Internal, "text_color").unwrap();
-        let separator_color =
-            PropertyColor::wrap(node_ref, Role::Internal, "separator_color").unwrap();
+        let separator_shape =
+            PropertyShape::wrap(node_ref, Role::Internal, "separator_shape", 0).unwrap();
         let padding_x = PropertyFloat32::wrap(node_ref, Role::Internal, "padding_x", 0).unwrap();
         let padding_y = PropertyFloat32::wrap(node_ref, Role::Internal, "padding_y", 0).unwrap();
         let z_index = PropertyUint32::wrap(node_ref, Role::Internal, "z_index", 0).unwrap();
@@ -113,7 +114,7 @@ impl TokenTable {
             priority,
             font_size,
             text_color,
-            separator_color,
+            separator_shape,
             padding_x,
             padding_y,
             draw_cache,
@@ -227,10 +228,13 @@ impl TokenTable {
         let rows = self.rows.lock();
         let font_size = self.font_size.get();
         let text_color = self.text_color.get();
-        let separator_color = self.separator_color.get();
         let padding_x = self.padding_x.get();
         let padding_y = self.padding_y.get();
 
+        let sep = self.separator_shape.get();
+        let sep_verts = sep.eval(rect.w + 1., 1.).expect("tokentable separator shape eval");
+        let sep_indices = sep.indices.clone();
+
         let mut instrs = vec![];
 
         for (i, row) in rows.iter().enumerate() {
@@ -260,7 +264,7 @@ impl TokenTable {
             // Draw separator line at bottom of row
             instrs.push(DrawInstruction::SetPos(Point::new(0., y_pos + row_height)));
             let mut mesh = MeshBuilder::new(gfxtag!("tokentable_separator"));
-            mesh.draw_line(Point::new(0., 0.), Point::new(rect.w + 1., 0.), separator_color, 1.);
+            mesh.append(sep_verts.clone(), sep_indices.clone());
             let mesh = mesh.alloc(&self.renderer);
             instrs.push(DrawInstruction::Draw(mesh.draw_untextured()));
         }
@@ -312,7 +316,7 @@ impl UIObject for TokenTable {
             self_.draw_cache.clear();
             self_.redraw.trigger();
         });
-        on_modify.when_change_external(self.separator_color.prop(), |self_, _| async move {
+        on_modify.when_change_external(self.separator_shape.prop(), |self_, _| async move {
             self_.draw_cache.clear();
             self_.redraw.trigger();
         });
@@ -352,7 +356,7 @@ impl UIObject for TokenTable {
         self.rect.eval(atom, &parent_rect).ok()?;
         // Wired colors (theme tokens)
         self.text_color.eval(atom).expect("text_color");
-        self.separator_color.eval(atom).expect("separator_color");
+        self.separator_shape.eval(atom).expect("separator_shape");
         let rect = self.rect.get();
         let rect_changed = rect != prev_rect;