Browse Source

app: add icons for close and copy selection when selecting lines in chatview

x 1 day ago
parent
commit
e526cfb8d7

+ 22 - 10
bin/app/src/app/schema/chat.rs

@@ -33,7 +33,8 @@ use crate::{
         App,
     },
     expr::{self, Compiler},
-    gfx::{gfxtag, Renderer},
+    gfx::{gfxtag, Point, Renderer},
+    mesh::{rgba, COLOR_RED, COLOR_WHITE},
     prop::{
         Property, PropertyAtomicGuard, PropertyBool, PropertyFloat32, PropertyStr, PropertySubType,
         PropertyType, Role,
@@ -166,6 +167,9 @@ mod ui_consts {
     pub const ACTION_PADDING: f32 = 8.;
     pub const ACTION_SPACING: f32 = 4.;
     pub const BACK_SEP_W: f32 = 0.5;
+
+    pub const COPY_BTN_SCALE: f32 = 20.;
+    pub const COPY_BTN_X_OFF: f32 = 30.;
 }
 
 use super::{EMOJI_PICKER_ICON_MARGIN_X, EMOJI_PICKER_ICON_MARGIN_Y, EMOJI_PICKER_ICON_SIZE};
@@ -217,6 +221,7 @@ pub async fn make(
     cc.add_const_f32("SENDARROW_NEG_X", SENDARROW_NEG_X);
     cc.add_const_f32("SENDARROW_NEG_Y", SENDARROW_NEG_Y);
     cc.add_const_f32("EMOJI_NEG_Y", EMOJI_NEG_Y);
+    cc.add_const_f32("COPY_BTN_X_OFF", COPY_BTN_X_OFF);
     cc.add_const_f32("EMOJIBTN_BOX_1", EMOJIBTN_BOX[1]);
     cc.add_const_f32("SENDBTN_BOX_0", SENDBTN_BOX[0]);
     cc.add_const_f32("SENDBTN_BOX_1", SENDBTN_BOX[1]);
@@ -686,7 +691,7 @@ pub async fn make(
         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).
+    // Background box covering the back button
     let node = create_vector_art("select_bg");
     let prop = node.get_property("rect").unwrap();
     prop.set_f32(atom, Role::App, 0, 0.).unwrap();
@@ -694,20 +699,27 @@ pub async fn make(
     prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
     prop.set_f32(atom, Role::App, 3, CHATEDIT_HEIGHT).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
-    let bg_color = match COLOR_SCHEME {
-        ColorScheme::DarkMode => [0., 0.11, 0.11, 1.],
-        ColorScheme::PaperLight => [1., 1., 1., 1.],
-    };
     let mut shape = VectorShape::new();
-    /*
     shape.add_filled_box(
         expr::const_f32(0.),
         expr::const_f32(0.),
-        expr::load_var("w"),
+        expr::const_f32(EMOJI_BG_W),
         expr::load_var("h"),
-        bg_color,
+        rgba!(0x1e0000ff),
     );
-    */
+    shape.join(shape::create_x(Point::new(40., 30.), 10., 1., COLOR_RED));
+    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
+    select_layer.link(node);
+
+    let node = create_vector_art("copy_bg");
+    let prop = node.get_property("rect").unwrap();
+    let code = cc.compile("w - COPY_BTN_X_OFF").unwrap();
+    prop.set_expr(atom, Role::App, 0, code).unwrap();
+    prop.set_f32(atom, Role::App, 1, 27.).unwrap();
+    prop.set_f32(atom, Role::App, 2, 400.).unwrap();
+    prop.set_f32(atom, Role::App, 3, CHATEDIT_HEIGHT).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
+    let shape = shape::create_copy(COLOR_WHITE).scaled(COPY_BTN_SCALE);
     let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
     select_layer.link(node);
 

+ 72 - 35
bin/app/src/app/schema/mod.rs

@@ -71,6 +71,24 @@ mod android_ui_consts {
 
     pub const SPLASH_FONTSIZE: f32 = 52.;
     pub const SPLASH_MARGIN: f32 = 40.;
+
+    pub const NETSTAT_OVERLAY_HEIGHT: f32 = 760.;
+    pub const NETSTAT_OVERLAY_SEP_X: f32 = 2.;
+    pub const NETSTAT_OVERLAY_SEP_Y: f32 = 240.;
+    pub const NETSTAT_OVERLAY_SEP_H: f32 = 2.;
+    pub const NETSTAT_OVERLAY_OUTLINE_W: f32 = 4.;
+    pub const NETSTAT_OVERLAY_TEXT_X: f32 = 100.;
+    pub const NETSTAT_OVERLAY_TEXT_MAX: f32 = 4000.;
+    pub const NETSTAT_OVERLAY_P2P_LABEL_Y: f32 = 100.;
+    pub const NETSTAT_OVERLAY_OUTBOUND_LABEL_Y: f32 = 340.;
+    pub const NETSTAT_OVERLAY_CONN_INFO_Y: f32 = 460.;
+    pub const NETSTAT_OVERLAY_TOGGLE_NEG_X: f32 = 240.;
+    pub const NETSTAT_OVERLAY_TOGGLE_R_PAD: f32 = 40.;
+    pub const NETSTAT_OVERLAY_TOGGLE_Y: f32 = 40.;
+    pub const NETSTAT_OVERLAY_TOGGLE_W: f32 = 200.;
+    pub const NETSTAT_OVERLAY_TOGGLE_H: f32 = 160.;
+    pub const NETSTAT_OVERLAY_TOGGLE_OUTLINE_W: f32 = 2.;
+    pub const NETSTAT_OVERLAY_TOGGLE_LABEL_Y: f32 = 90.;
 }
 
 #[cfg(target_os = "android")]
@@ -140,6 +158,24 @@ mod ui_consts {
     pub const SPLASH_FONTSIZE: f32 = 26.;
     pub const SPLASH_MARGIN: f32 = 20.;
 
+    pub const NETSTAT_OVERLAY_HEIGHT: f32 = 380.;
+    pub const NETSTAT_OVERLAY_SEP_X: f32 = 1.;
+    pub const NETSTAT_OVERLAY_SEP_Y: f32 = 120.;
+    pub const NETSTAT_OVERLAY_SEP_H: f32 = 1.;
+    pub const NETSTAT_OVERLAY_OUTLINE_W: f32 = 2.;
+    pub const NETSTAT_OVERLAY_TEXT_X: f32 = 50.;
+    pub const NETSTAT_OVERLAY_TEXT_MAX: f32 = 2000.;
+    pub const NETSTAT_OVERLAY_P2P_LABEL_Y: f32 = 50.;
+    pub const NETSTAT_OVERLAY_OUTBOUND_LABEL_Y: f32 = 170.;
+    pub const NETSTAT_OVERLAY_CONN_INFO_Y: f32 = 230.;
+    pub const NETSTAT_OVERLAY_TOGGLE_NEG_X: f32 = 120.;
+    pub const NETSTAT_OVERLAY_TOGGLE_R_PAD: f32 = 20.;
+    pub const NETSTAT_OVERLAY_TOGGLE_Y: f32 = 20.;
+    pub const NETSTAT_OVERLAY_TOGGLE_W: f32 = 100.;
+    pub const NETSTAT_OVERLAY_TOGGLE_H: f32 = 80.;
+    pub const NETSTAT_OVERLAY_TOGGLE_OUTLINE_W: f32 = 1.;
+    pub const NETSTAT_OVERLAY_TOGGLE_LABEL_Y: f32 = 45.;
+
     pub use super::desktop_paths::*;
 }
 
@@ -223,6 +259,8 @@ pub async fn make(
     cc.add_const_f32("NETSTAT_OVERLAY_MARGIN", NETSTAT_OVERLAY_MARGIN);
     cc.add_const_f32("NETSTAT_OVERLAY_BTN_W", NETSTAT_OVERLAY_BTN_W);
     cc.add_const_f32("NETSTAT_OVERLAY_BTN_H", NETSTAT_OVERLAY_BTN_H);
+    cc.add_const_f32("NETSTAT_OVERLAY_TOGGLE_NEG_X", NETSTAT_OVERLAY_TOGGLE_NEG_X);
+    cc.add_const_f32("NETSTAT_OVERLAY_TOGGLE_R_PAD", NETSTAT_OVERLAY_TOGGLE_R_PAD);
 
     let atom = &mut PropertyAtomicGuard::none();
 
@@ -645,7 +683,7 @@ pub async fn make(
     prop.set_expr(atom, Role::App, 2, code).unwrap();
     //let code = cc.compile("h - NETSTATUS_ICON_SIZE - 2 * NETSTAT_OVERLAY_MARGIN").unwrap();
     //prop.set_expr(atom, Role::App, 3, code).unwrap();
-    prop.set_f32(atom, Role::App, 3, 380.).unwrap();
+    prop.set_f32(atom, Role::App, 3, NETSTAT_OVERLAY_HEIGHT).unwrap();
     overlay_node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
     overlay_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     overlay_node.set_property_u32(atom, Role::App, "priority", 2).unwrap();
@@ -663,7 +701,6 @@ pub async fn make(
     prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
     node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
-    let overlay_color = [0.05, 0.25, 0.3, 0.8];
     let mut shape = VectorShape::new();
     shape.add_filled_box(
         expr::const_f32(0.),
@@ -673,10 +710,10 @@ pub async fn make(
         [0., 0.1, 0.1, 0.7],
     );
     shape.add_filled_box(
-        expr::const_f32(1.),
-        expr::const_f32(120.),
+        expr::const_f32(NETSTAT_OVERLAY_SEP_X),
+        expr::const_f32(NETSTAT_OVERLAY_SEP_Y),
         expr::load_var("w"),
-        expr::const_f32(121.),
+        expr::const_f32(NETSTAT_OVERLAY_SEP_Y + NETSTAT_OVERLAY_SEP_H),
         [0., 0.94, 1., 1.],
     );
     shape.add_outline(
@@ -684,22 +721,22 @@ pub async fn make(
         expr::const_f32(0.),
         expr::load_var("w"),
         expr::load_var("h"),
-        2.,
+        NETSTAT_OVERLAY_OUTLINE_W,
         [0., 0.94, 1., 1.],
     );
     shape.add_filled_box(
-        cc.compile("w - 120").unwrap(),
-        expr::const_f32(20.),
-        cc.compile("w - 20").unwrap(),
-        expr::const_f32(100.),
+        cc.compile("w - NETSTAT_OVERLAY_TOGGLE_NEG_X").unwrap(),
+        expr::const_f32(NETSTAT_OVERLAY_TOGGLE_Y),
+        cc.compile("w - NETSTAT_OVERLAY_TOGGLE_R_PAD").unwrap(),
+        expr::const_f32(NETSTAT_OVERLAY_TOGGLE_Y + NETSTAT_OVERLAY_TOGGLE_H),
         [0., 0.12, 0.08, 1.],
     );
     shape.add_outline(
-        cc.compile("w - 120").unwrap(),
-        expr::const_f32(20.),
-        cc.compile("w - 20").unwrap(),
-        expr::const_f32(100.),
-        1.,
+        cc.compile("w - NETSTAT_OVERLAY_TOGGLE_NEG_X").unwrap(),
+        expr::const_f32(NETSTAT_OVERLAY_TOGGLE_Y),
+        cc.compile("w - NETSTAT_OVERLAY_TOGGLE_R_PAD").unwrap(),
+        expr::const_f32(NETSTAT_OVERLAY_TOGGLE_Y + NETSTAT_OVERLAY_TOGGLE_H),
+        NETSTAT_OVERLAY_TOGGLE_OUTLINE_W,
         [0.08, 0.68, 0.72, 1.],
     );
     let node = node
@@ -709,10 +746,10 @@ pub async fn make(
 
     let node = create_text("p2p_label");
     let prop = node.get_property("rect").unwrap();
-    prop.set_f32(atom, Role::App, 0, 50.).unwrap();
-    prop.set_f32(atom, Role::App, 1, 50.).unwrap();
-    prop.set_f32(atom, Role::App, 2, 2000.).unwrap();
-    prop.set_f32(atom, Role::App, 3, 2000.).unwrap();
+    prop.set_f32(atom, Role::App, 0, NETSTAT_OVERLAY_TEXT_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, NETSTAT_OVERLAY_P2P_LABEL_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, NETSTAT_OVERLAY_TEXT_MAX).unwrap();
+    prop.set_f32(atom, Role::App, 3, NETSTAT_OVERLAY_TEXT_MAX).unwrap();
     node.set_property_f32(atom, Role::App, "font_size", NETSTAT_OVERLAY_BTN_FONTSIZE).unwrap();
     node.set_property_str(atom, Role::App, "text", "P2P").unwrap();
     node.set_property_enum(atom, Role::App, "text_align", "left").unwrap();
@@ -737,11 +774,11 @@ pub async fn make(
 
     let node = create_text("toggle_label");
     let prop = node.get_property("rect").unwrap();
-    let code = cc.compile("w - 120").unwrap();
+    let code = cc.compile("w - NETSTAT_OVERLAY_TOGGLE_NEG_X").unwrap();
     prop.set_expr(atom, Role::App, 0, code).unwrap();
-    prop.set_f32(atom, Role::App, 1, 45.).unwrap();
-    prop.set_f32(atom, Role::App, 2, 100.).unwrap();
-    prop.set_f32(atom, Role::App, 3, 2000.).unwrap();
+    prop.set_f32(atom, Role::App, 1, NETSTAT_OVERLAY_TOGGLE_LABEL_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, NETSTAT_OVERLAY_TOGGLE_W).unwrap();
+    prop.set_f32(atom, Role::App, 3, NETSTAT_OVERLAY_TEXT_MAX).unwrap();
     node.set_property_f32(atom, Role::App, "font_size", NETSTAT_OVERLAY_BTN_FONTSIZE).unwrap();
     node.set_property_str(atom, Role::App, "text", "on").unwrap();
     node.set_property_enum(atom, Role::App, "text_align", "center").unwrap();
@@ -774,11 +811,11 @@ pub async fn make(
     let node = create_button("p2p_toggle_btn");
     node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
     let prop = node.get_property("rect").unwrap();
-    let code = cc.compile("w - 120").unwrap();
+    let code = cc.compile("w - NETSTAT_OVERLAY_TOGGLE_NEG_X").unwrap();
     prop.set_expr(atom, Role::App, 0, code).unwrap();
-    prop.set_f32(atom, Role::App, 1, 20.).unwrap();
-    prop.set_f32(atom, Role::App, 2, 100.).unwrap();
-    prop.set_f32(atom, Role::App, 3, 80.).unwrap();
+    prop.set_f32(atom, Role::App, 1, NETSTAT_OVERLAY_TOGGLE_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, NETSTAT_OVERLAY_TOGGLE_W).unwrap();
+    prop.set_f32(atom, Role::App, 3, NETSTAT_OVERLAY_TOGGLE_H).unwrap();
     let (slot, recvr) = Slot::new("toggle_p2p");
     node.register("click", slot).unwrap();
     let redraw = app.redraw_trigger.clone();
@@ -802,10 +839,10 @@ pub async fn make(
 
     let node = create_text("outbound_label");
     let prop = node.get_property("rect").unwrap();
-    prop.set_f32(atom, Role::App, 0, 50.).unwrap();
-    prop.set_f32(atom, Role::App, 1, 170.).unwrap();
-    prop.set_f32(atom, Role::App, 2, 2000.).unwrap();
-    prop.set_f32(atom, Role::App, 3, 2000.).unwrap();
+    prop.set_f32(atom, Role::App, 0, NETSTAT_OVERLAY_TEXT_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, NETSTAT_OVERLAY_OUTBOUND_LABEL_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, NETSTAT_OVERLAY_TEXT_MAX).unwrap();
+    prop.set_f32(atom, Role::App, 3, NETSTAT_OVERLAY_TEXT_MAX).unwrap();
     node.set_property_f32(atom, Role::App, "font_size", NETSTAT_OVERLAY_BTN_FONTSIZE).unwrap();
     node.set_property_str(atom, Role::App, "text", "OUTBOUND").unwrap();
     node.set_property_enum(atom, Role::App, "text_align", "left").unwrap();
@@ -830,10 +867,10 @@ pub async fn make(
 
     let node = create_text("conn_info");
     let prop = node.get_property("rect").unwrap();
-    prop.set_f32(atom, Role::App, 0, 50.).unwrap();
-    prop.set_f32(atom, Role::App, 1, 230.).unwrap();
-    prop.set_f32(atom, Role::App, 2, 2000.).unwrap();
-    prop.set_f32(atom, Role::App, 3, 2000.).unwrap();
+    prop.set_f32(atom, Role::App, 0, NETSTAT_OVERLAY_TEXT_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, NETSTAT_OVERLAY_CONN_INFO_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, NETSTAT_OVERLAY_TEXT_MAX).unwrap();
+    prop.set_f32(atom, Role::App, 3, NETSTAT_OVERLAY_TEXT_MAX).unwrap();
     node.set_property_f32(atom, Role::App, "font_size", NETSTAT_OVERLAY_BTN_FONTSIZE).unwrap();
     #[cfg(not(feature = "enable-plugin-darkirc"))]
     node.set_property_str(

+ 30 - 0
bin/app/src/app/schema/wallet/main.rs

@@ -75,6 +75,36 @@ pub async fn make(
     create_bg_mesh(app, atom, &main_layer, "wallet_bg").await;
     create_header_bg(app, atom, &main_layer, "wallet_header_bg").await;
 
+    // Back arrow
+    let node = create_vector_art("wallet_back_btn_bg");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, BACKARROW_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, BACKARROW_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, BACKARROW_SCALE).unwrap();
+    prop.set_f32(atom, Role::App, 3, BACKARROW_SCALE).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
+    let arrow_shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
+    let node = node
+        .setup(|me| {
+            VectorArt::new(me, arrow_shape, app.renderer.clone(), app.redraw_trigger.clone())
+        })
+        .await;
+    main_layer.link(node);
+
+    // Back button
+    let node = create_button("wallet_back_btn");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 10).unwrap();
+    node.set_property_u32(atom, Role::App, "priority", 10).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 2, BACKARROW_BG_W).unwrap();
+    prop.set_f32(atom, Role::App, 3, HEADER_HEIGHT).unwrap();
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
+    main_layer.link(node);
+
     create_chat_btn(app, atom, &cc, &main_layer).await;
 
     let mut y = HEADER_HEIGHT;

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

@@ -65,7 +65,29 @@ pub async fn make(
     create_bg_mesh(app, atom, &receive_layer, "receive_bg").await;
     create_header_bg(app, atom, &receive_layer, "receive_header_bg").await;
 
-    let node = create_back_btn(app, atom, &receive_layer, "receive_back_btn").await;
+    // Back button
+    let node = create_vector_art("receive_back_btn_bg");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, BACKARROW_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, BACKARROW_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, 500.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 500.).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
+    receive_layer.link(node);
+
+    let mut y = 0.;
+
+    let node = create_button("receive_back_btn");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, y).unwrap();
+    prop.set_f32(atom, Role::App, 2, WALLET_BTN_SIZE * 2.).unwrap();
+    prop.set_f32(atom, Role::App, 3, HEADER_HEIGHT).unwrap();
 
     let main_is_visible = PropertyBool::wrap(&main_layer, Role::App, "is_visible", 0).unwrap();
     let receive_is_visible =
@@ -82,7 +104,11 @@ pub async fn make(
     });
     app.tasks.lock().unwrap().push(listen_click);
 
-    let mut y = HEADER_HEIGHT;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
+    receive_layer.link(node);
+
+    y += HEADER_HEIGHT;
 
     create_title(app, atom, &receive_layer, &window_scale, i18n_fish, "RECEIVE", &mut y).await;
 

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

@@ -22,7 +22,7 @@ use darkfi_serial::Decodable;
 
 use crate::{
     app::{
-        node::{create_button, create_layer, create_tokentable},
+        node::{create_button, create_layer, create_tokentable, create_vector_art},
         schema::COLOR_SCHEME,
         App,
     },
@@ -30,7 +30,8 @@ use crate::{
     gfx::gfxtag,
     prop::{PropertyAtomicGuard, PropertyBool, PropertyFloat32, Role},
     scene::{SceneNodePtr, Slot},
-    ui::{Button, Layer, TokenRow, TokenTable},
+    shape,
+    ui::{Button, Layer, TokenRow, TokenTable, VectorArt},
     util::i18n::I18nBabelFish,
 };
 
@@ -80,7 +81,29 @@ pub async fn make(
     create_bg_mesh(app, atom, &send_step1_layer, "send_bg").await;
     create_header_bg(app, atom, &send_step1_layer, "send_header_bg").await;
 
-    let node = create_back_btn(app, atom, &send_step1_layer, "send_back_btn").await;
+    // Back button
+    let node = create_vector_art("send_back_btn_bg");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, BACKARROW_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, BACKARROW_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, 500.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 500.).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
+    send_step1_layer.link(node);
+
+    let mut y = 0.;
+
+    let node = create_button("send_back_btn");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, y).unwrap();
+    prop.set_f32(atom, Role::App, 2, WALLET_BTN_SIZE * 2.).unwrap();
+    prop.set_f32(atom, Role::App, 3, HEADER_HEIGHT).unwrap();
 
     let main_is_visible = PropertyBool::wrap(&main_layer, Role::App, "is_visible", 0).unwrap();
     let redraw = app.redraw_trigger.clone();
@@ -97,7 +120,11 @@ pub async fn make(
     });
     app.tasks.lock().unwrap().push(listen_click);
 
-    let mut y = HEADER_HEIGHT;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
+    send_step1_layer.link(node);
+
+    y += HEADER_HEIGHT;
 
     create_title(app, atom, &send_step1_layer, &window_scale, i18n_fish, "SEND", &mut y).await;
 

+ 28 - 2
bin/app/src/app/schema/wallet/send_step2.rs

@@ -110,7 +110,29 @@ pub async fn make(
     create_bg_mesh(app, atom, &send_step2_layer, "send_bg2").await;
     create_header_bg(app, atom, &send_step2_layer, "send_header_bg2").await;
 
-    let node = create_back_btn(app, atom, &send_step2_layer, "send_back_btn2").await;
+    // Back button
+    let node = create_vector_art("send_back_btn_bg2");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, BACKARROW_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, BACKARROW_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, 500.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 500.).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
+    send_step2_layer.link(node);
+
+    let mut y = 0.;
+
+    let node = create_button("send_back_btn2");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, y).unwrap();
+    prop.set_f32(atom, Role::App, 2, WALLET_BTN_SIZE * 2.).unwrap();
+    prop.set_f32(atom, Role::App, 3, HEADER_HEIGHT).unwrap();
 
     let step1_is_visible2 = step1_is_visible.clone();
     let step2_is_visible1 = step2_is_visible.clone();
@@ -126,7 +148,11 @@ pub async fn make(
     });
     app.tasks.lock().unwrap().push(listen_click);
 
-    let mut y = HEADER_HEIGHT;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
+    send_step2_layer.link(node);
+
+    y += HEADER_HEIGHT;
 
     create_title(app, atom, &send_step2_layer, &window_scale, i18n_fish, "SEND", &mut y).await;
 

+ 31 - 3
bin/app/src/app/schema/wallet/send_step3.rs

@@ -25,6 +25,7 @@ use crate::{
     app::{
         node::{
             create_button, create_decimal_edit, create_layer, create_singleline_edit, create_text,
+            create_vector_art,
         },
         schema::COLOR_SCHEME,
         App,
@@ -34,7 +35,8 @@ use crate::{
     mesh::COLOR_CYAN,
     prop::{PropertyAtomicGuard, PropertyBool, PropertyFloat32, PropertyRect, Role},
     scene::{SceneNodePtr, Slot},
-    ui::{BaseEdit, BaseEditType, Button, Layer, Text},
+    shape,
+    ui::{BaseEdit, BaseEditType, Button, Layer, Text, VectorArt},
     util::i18n::I18nBabelFish,
 };
 
@@ -83,7 +85,29 @@ pub async fn make(
     create_bg_mesh(app, atom, &send_step3_layer, "send_bg3").await;
     create_header_bg(app, atom, &send_step3_layer, "send_header_bg3").await;
 
-    let node = create_back_btn(app, atom, &send_step3_layer, "send_back_btn3").await;
+    // Back button
+    let node = create_vector_art("send_back_btn_bg3");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, BACKARROW_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, BACKARROW_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, 500.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 500.).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
+    send_step3_layer.link(node);
+
+    let mut y = 0.;
+
+    let node = create_button("send_back_btn3");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, y).unwrap();
+    prop.set_f32(atom, Role::App, 2, WALLET_BTN_SIZE * 2.).unwrap();
+    prop.set_f32(atom, Role::App, 3, HEADER_HEIGHT).unwrap();
 
     let step2_is_visible3 = step2_is_visible.clone();
     let step3_is_visible1 = step3_is_visible.clone();
@@ -106,7 +130,11 @@ pub async fn make(
     });
     app.tasks.lock().unwrap().push(listen_click);
 
-    let mut y = HEADER_HEIGHT;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
+    send_step3_layer.link(node);
+
+    y += HEADER_HEIGHT;
 
     create_title(app, atom, &send_step3_layer, &window_scale, i18n_fish, "SEND", &mut y).await;
 

+ 31 - 4
bin/app/src/app/schema/wallet/send_step4.rs

@@ -22,7 +22,7 @@ use darkfi_serial::{Decodable, Encodable};
 
 use crate::{
     app::{
-        node::{create_button, create_layer, create_text},
+        node::{create_button, create_layer, create_text, create_vector_art},
         schema::COLOR_SCHEME,
         App,
     },
@@ -30,7 +30,8 @@ use crate::{
     gfx::gfxtag,
     prop::{PropertyAtomicGuard, PropertyBool, PropertyFloat32, Role},
     scene::{SceneNodePtr, Slot},
-    ui::{Button, Layer, Text},
+    shape,
+    ui::{Button, Layer, Text, VectorArt},
     util::i18n::I18nBabelFish,
 };
 
@@ -79,7 +80,29 @@ pub async fn make(
     create_bg_mesh(app, atom, &send_step4_layer, "send_bg4").await;
     create_header_bg(app, atom, &send_step4_layer, "send_header_bg4").await;
 
-    let node = create_back_btn(app, atom, &send_step4_layer, "send_back_btn4").await;
+    // Back button
+    let node = create_vector_art("send_back_btn_bg4");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, BACKARROW_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, BACKARROW_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, 500.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 500.).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
+    send_step4_layer.link(node);
+
+    let mut y = 0.;
+
+    let node = create_button("send_back_btn4");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, y).unwrap();
+    prop.set_f32(atom, Role::App, 2, WALLET_BTN_SIZE * 2.).unwrap();
+    prop.set_f32(atom, Role::App, 3, HEADER_HEIGHT).unwrap();
 
     let step3_is_visible2 = step3_is_visible.clone();
     let step4_is_visible1 = step4_is_visible.clone();
@@ -95,7 +118,11 @@ pub async fn make(
     });
     app.tasks.lock().unwrap().push(listen_click);
 
-    let mut y = HEADER_HEIGHT;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
+    send_step4_layer.link(node);
+
+    y += HEADER_HEIGHT;
 
     create_title(app, atom, &send_step4_layer, &window_scale, i18n_fish, "SEND", &mut y).await;
 

+ 8 - 61
bin/app/src/app/schema/wallet/util.rs

@@ -34,7 +34,7 @@ use crate::{
     mesh::{COLOR_CYAN, COLOR_TEAL},
     prop::{PropertyAtomicGuard, PropertyFloat32, Role},
     scene::{Pimpl, SceneNodePtr},
-    shape, text,
+    text,
     ui::{Button, Layer, RedrawTrigger, Text, VectorArt, VectorShape},
     util::i18n::I18nBabelFish,
 };
@@ -294,6 +294,13 @@ pub async fn create_header_bg(
         expr::load_var("h"),
         bg_color,
     );
+    shape.add_filled_box(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::const_f32(BACKARROW_BG_W),
+        expr::load_var("h"),
+        [0.0, 0.106, 0.114, 1.0],
+    );
     shape.add_filled_box(
         expr::const_f32(BACKARROW_BG_W),
         expr::const_f32(0.),
@@ -749,63 +756,3 @@ pub async fn create_tooltip(
 
     (tooltip_layer, width)
 }
-
-/// Creates a back button with back arrow shape, colored background, and button.
-/// Returns the button node after setup, linked to the layer.
-pub async fn create_back_btn(
-    app: &App,
-    atom: &mut PropertyAtomicGuard,
-    layer: &SceneNodePtr,
-    name: &str,
-) -> SceneNodePtr {
-    // Colored background box
-    let bg_node = create_vector_art(&format!("{}_colored_bg", name));
-    let prop = bg_node.get_property("rect").unwrap();
-    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
-    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
-    prop.set_f32(atom, Role::App, 2, BACKARROW_BG_W).unwrap();
-    prop.set_f32(atom, Role::App, 3, HEADER_HEIGHT).unwrap();
-    bg_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::const_f32(BACKARROW_BG_W),
-        expr::load_var("h"),
-        [0.0, 0.106, 0.114, 1.0],
-    );
-    let bg_node = bg_node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
-    layer.link(bg_node);
-
-    // Back arrow shape
-    let arrow_node = create_vector_art(&format!("{}_arrow", name));
-    let prop = arrow_node.get_property("rect").unwrap();
-    prop.set_f32(atom, Role::App, 0, BACKARROW_X).unwrap();
-    prop.set_f32(atom, Role::App, 1, BACKARROW_Y).unwrap();
-    prop.set_f32(atom, Role::App, 2, BACKARROW_SCALE).unwrap();
-    prop.set_f32(atom, Role::App, 3, BACKARROW_SCALE).unwrap();
-    arrow_node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
-    let arrow_shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
-    let arrow_node = arrow_node
-        .setup(|me| {
-            VectorArt::new(me, arrow_shape, app.renderer.clone(), app.redraw_trigger.clone())
-        })
-        .await;
-    layer.link(arrow_node);
-
-    let node = create_button(name);
-    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
-    node.set_property_u32(atom, Role::App, "z_index", 10).unwrap();
-    node.set_property_u32(atom, Role::App, "priority", 10).unwrap();
-    let prop = node.get_property("rect").unwrap();
-    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
-    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
-    prop.set_f32(atom, Role::App, 2, BACKARROW_BG_W).unwrap();
-    prop.set_f32(atom, Role::App, 3, HEADER_HEIGHT).unwrap();
-    let node =
-        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
-    layer.link(node.clone());
-    node
-}

+ 15 - 0
bin/app/src/mesh.rs

@@ -22,6 +22,21 @@ use crate::gfx::{
 
 pub type Color = [f32; 4];
 
+// rgba!(0xRRGGBBAA) expands to a [f32; 4] with each channel normalized to 0. .. 1.
+#[macro_export]
+macro_rules! rgba {
+    ($c:expr) => {{
+        let c: u32 = $c;
+        [
+            ((c >> 24) & 0xFF) as f32 / 255.,
+            ((c >> 16) & 0xFF) as f32 / 255.,
+            ((c >> 8) & 0xFF) as f32 / 255.,
+            (c & 0xFF) as f32 / 255.,
+        ]
+    }};
+}
+pub use crate::rgba;
+
 #[allow(dead_code)]
 pub const COLOR_RED: Color = [1., 0., 0., 1.];
 #[allow(dead_code)]

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

@@ -58,6 +58,9 @@ pub use blockchain_netlogo3::create_blockchain_netlogo3;
 mod blockchain_netlogo4;
 pub use blockchain_netlogo4::create_blockchain_netlogo4;
 
+mod x;
+pub use x::create_x;
+
 //mod settings;
 //pub use settings::{create_right_border, create_settings};
 

+ 36 - 0
bin/app/src/shape/x.rs

@@ -0,0 +1,36 @@
+/* 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::{gfx::Point, mesh::Color, ui::VectorShape};
+
+pub fn create_x(center: Point, width: f32, thickness: f32, color: Color) -> VectorShape {
+    let mut shape = VectorShape::new();
+    shape.add_line(
+        Point::new(center.x - width, center.y - width),
+        Point::new(center.x + width, center.y + width),
+        thickness,
+        color.clone(),
+    );
+    shape.add_line(
+        Point::new(center.x + width, center.y - width),
+        Point::new(center.x - width, center.y + width),
+        thickness,
+        color,
+    );
+    shape
+}

+ 23 - 1
bin/app/src/ui/vector_art/shape.rs

@@ -19,7 +19,7 @@
 use crate::{
     error::Result,
     expr::{Op, SExprCode, SExprMachine, SExprVal},
-    gfx::Vertex,
+    gfx::{Point, Vertex},
     mesh::Color,
 };
 
@@ -253,6 +253,28 @@ impl VectorShape {
         );
     }
 
+    /// Draw a line of a certain thickness between two points.
+    /// Coordinates are constants, so this does not track expressions like `w` or `h`.
+    pub fn add_line(&mut self, from: Point, to: Point, thickness: f32, color: Color) {
+        let dx = to.x - from.x;
+        let dy = to.y - from.y;
+        let length = (dx * dx + dy * dy).sqrt();
+        if length == 0. {
+            return
+        }
+
+        let half = thickness / 2.;
+        let px = -dy / length * half;
+        let py = dx / length * half;
+
+        let i = self.verts.len() as u16;
+        self.verts.push(ShapeVertex::from_xy(from.x + px, from.y + py, color.clone()));
+        self.verts.push(ShapeVertex::from_xy(to.x + px, to.y + py, color.clone()));
+        self.verts.push(ShapeVertex::from_xy(from.x - px, from.y - py, color.clone()));
+        self.verts.push(ShapeVertex::from_xy(to.x - px, to.y - py, color));
+        self.indices.extend([i, i + 2, i + 1, i + 1, i + 2, i + 3]);
+    }
+
     pub fn add_radial_glow(
         &mut self,
         center_x: SExprCode,