Просмотр исходного кода

app: add light theme mostly working

darkfi 2 недель назад
Родитель
Сommit
69960b8259

+ 0 - 59
bin/app/src/app/schema/chat.rs

@@ -104,7 +104,6 @@ mod android_ui_consts {
     // Action menu
     pub const ACTION_PADDING: f32 = 32.;
     pub const ACTION_SPACING: f32 = 8.;
-    pub const BACK_SEP_W: f32 = 1.;
 
     pub const COPY_BTN_SCALE: f32 = 38.;
     pub const COPY_BTN_X_OFF: f32 = 70.;
@@ -188,7 +187,6 @@ mod ui_consts {
     // Action menu
     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 = 19.;
     pub const COPY_BTN_X_OFF: f32 = 30.;
@@ -282,63 +280,6 @@ pub async fn make(
     let layer_node = layer_node.setup(|me| Layer::new(me, renderer.clone(), redraw.clone())).await;
     content.link(layer_node.clone());
 
-    // Create the toolbar bg
-    let node = create_vector_art("toolbar_bg");
-    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_f32(3, CHATEDIT_HEIGHT).unwrap();
-    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
-
-    let (bg_color, sep_color) = ([0., 0., 0., 1.], [0.41, 0.6, 0.65, 1.]);
-    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"),
-        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.),
-        expr::const_f32(BACKARROW_BG_W + BACK_SEP_W),
-        expr::load_var("h"),
-        sep_color,
-    );
-    shape.add_filled_box(
-        expr::const_f32(0.),
-        expr::load_var("h"),
-        expr::load_var("w"),
-        cc.compile("h + 0.5").unwrap(),
-        sep_color,
-    );
-    let color1 = [0.2, 0.2, 0.2, 0.5];
-    let color2 = [0.5, 0.5, 0.5, 0.];
-    shape.add_smooth_vertical_gradient(
-        expr::const_f32(BACKARROW_BG_W + 1.),
-        expr::const_f32(0.),
-        expr::load_var("w"),
-        cc.compile("h / 2").unwrap(),
-        color1,
-        color2,
-        8,
-        0.2,
-    );
-
-    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
-
-    let node = node.setup(|me| VectorArt::new(me, renderer.clone(), redraw.clone())).await;
-    layer_node.link(node);
-
     // Create the send button
     let node = create_vector_art("back_btn_bg");
     let prop = node.get_property("rect").unwrap();

+ 0 - 59
bin/app/src/app/schema/menu/channel.rs

@@ -96,7 +96,6 @@ mod android_ui_consts {
     pub const COPY_SCALE: f32 = 35.;
     pub const COPY_BTN_SIZE: f32 = CHATEDIT_HEIGHT;
     pub const CONTENT_OUTLINE_SIZE: f32 = 0.5;
-    pub const BACK_SEP_W: f32 = 1.;
     pub const TAB_LABEL_X: f32 = 90.;
     pub const CHANNELS_TAB_ICON_GAP: f32 = 12.;
     pub const CONTACTS_TAB_ICON_GAP: f32 = 8.5;
@@ -148,7 +147,6 @@ mod ui_consts {
     pub const COPY_SCALE: f32 = 15.;
     pub const COPY_BTN_SIZE: f32 = CHATEDIT_HEIGHT;
     pub const CONTENT_OUTLINE_SIZE: f32 = 0.3;
-    pub const BACK_SEP_W: f32 = 0.5;
     pub const TAB_LABEL_X: f32 = 45.;
     pub const CHANNELS_TAB_ICON_GAP: f32 = 6.;
     pub const CONTACTS_TAB_ICON_GAP: f32 = 4.25;
@@ -192,63 +190,6 @@ pub async fn make(
 
     let atom = &mut PropertyAtomicGuard::none();
 
-    // Header
-    let node = create_vector_art("header_bg");
-    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_f32(3, HEADER_HEIGHT).unwrap();
-    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
-
-    let (bg_color, sep_color) = ([0., 0., 0., 1.], [0.41, 0.6, 0.65, 1.]);
-    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"),
-        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.),
-        expr::const_f32(BACKARROW_BG_W + BACK_SEP_W),
-        expr::load_var("h"),
-        sep_color,
-    );
-    shape.add_filled_box(
-        expr::const_f32(0.),
-        expr::load_var("h"),
-        expr::load_var("w"),
-        cc.compile("h + 0.5").unwrap(),
-        sep_color,
-    );
-    let color1 = [0.2, 0.2, 0.2, 0.5];
-    let color2 = [0.5, 0.5, 0.5, 0.];
-    shape.add_smooth_vertical_gradient(
-        expr::const_f32(BACKARROW_BG_W + 1.),
-        expr::const_f32(0.),
-        expr::load_var("w"),
-        cc.compile("h / 2").unwrap(),
-        color1,
-        color2,
-        8,
-        0.2,
-    );
-
-    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;
-    content.link(node);
-
     // Create back arrow
     let node = create_vector_art("back_btn_bg");
     let prop = node.get_property("rect").unwrap();

+ 0 - 59
bin/app/src/app/schema/menu/contact.rs

@@ -96,7 +96,6 @@ mod android_ui_consts {
     pub const COPY_SCALE: f32 = 35.;
     pub const COPY_BTN_SIZE: f32 = CHATEDIT_HEIGHT;
     pub const CONTENT_OUTLINE_SIZE: f32 = 0.5;
-    pub const BACK_SEP_W: f32 = 1.;
     pub const TAB_LABEL_X: f32 = 90.;
     pub const CHANNELS_TAB_ICON_GAP: f32 = 12.;
     pub const CONTACTS_TAB_ICON_GAP: f32 = 8.5;
@@ -148,7 +147,6 @@ mod ui_consts {
     pub const COPY_SCALE: f32 = 15.;
     pub const COPY_BTN_SIZE: f32 = CHATEDIT_HEIGHT;
     pub const CONTENT_OUTLINE_SIZE: f32 = 0.3;
-    pub const BACK_SEP_W: f32 = 0.5;
     pub const TAB_LABEL_X: f32 = 45.;
     pub const CHANNELS_TAB_ICON_GAP: f32 = 6.;
     pub const CONTACTS_TAB_ICON_GAP: f32 = 4.25;
@@ -193,63 +191,6 @@ pub async fn make(
 
     let atom = &mut PropertyAtomicGuard::none();
 
-    // Header
-    let node = create_vector_art("header_bg");
-    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_f32(3, HEADER_HEIGHT).unwrap();
-    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
-
-    let (bg_color, sep_color) = ([0., 0., 0., 1.], [0.41, 0.6, 0.65, 1.]);
-    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"),
-        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.),
-        expr::const_f32(BACKARROW_BG_W + BACK_SEP_W),
-        expr::load_var("h"),
-        sep_color,
-    );
-    shape.add_filled_box(
-        expr::const_f32(0.),
-        expr::load_var("h"),
-        expr::load_var("w"),
-        cc.compile("h + 0.5").unwrap(),
-        sep_color,
-    );
-    let color1 = [0.2, 0.2, 0.2, 0.5];
-    let color2 = [0.5, 0.5, 0.5, 0.];
-    shape.add_smooth_vertical_gradient(
-        expr::const_f32(BACKARROW_BG_W + 1.),
-        expr::const_f32(0.),
-        expr::load_var("w"),
-        cc.compile("h / 2").unwrap(),
-        color1,
-        color2,
-        8,
-        0.2,
-    );
-
-    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;
-    content.link(node);
-
     // Create back arrow
     let node = create_vector_art("back_btn_bg");
     let prop = node.get_property("rect").unwrap();

+ 32 - 87
bin/app/src/app/schema/menu/mod.rs

@@ -35,10 +35,7 @@ use crate::{
     scene::{SceneNodePtr, Slot},
     sfx, shape,
     theme::wire_color,
-    ui::{
-        emoji_picker::EmojiMeshesPtr, Button, Layer, Menu, ShapeVertex, Text, VectorArt,
-        VectorShape,
-    },
+    ui::{emoji_picker::EmojiMeshesPtr, Button, Layer, Menu, ShapeVertex, Text, VectorArt},
     util::i18n::I18nBabelFish,
 };
 
@@ -72,13 +69,8 @@ mod android_ui_consts {
     pub const DARKFI_LOGO_SCALE: f32 = 40.;
 }
 
-#[cfg(target_os = "android")]
-mod ui_consts {
-    pub use super::android_ui_consts::*;
-}
-
-#[cfg(feature = "emulate-android")]
-mod ui_consts {
+#[cfg(any(target_os = "android", feature = "emulate-android"))]
+pub mod ui_consts {
     pub use super::android_ui_consts::*;
 }
 
@@ -86,7 +78,7 @@ mod ui_consts {
     any(target_os = "linux", target_os = "macos", target_os = "windows"),
     not(feature = "emulate-android")
 ))]
-mod ui_consts {
+pub mod ui_consts {
     pub const CHANNEL_LABEL_X: f32 = 20.;
     pub const CHANNEL_HEADER_HEIGHT: f32 = 60.;
     pub const CHANNEL_ITEM_HEIGHT: f32 = 40.;
@@ -228,49 +220,6 @@ pub async fn make(
     )
     .await;
 
-    // Channels label bg
-    let node = create_vector_art("channels_label_bg");
-    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_f32(3, CHANNEL_HEADER_HEIGHT).unwrap();
-    node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
-
-    let mut shape = VectorShape::new();
-    let (bg_color, sep_color) = ([0., 0., 0., 1.], [0.41, 0.6, 0.65, 1.]);
-    shape.add_filled_box(
-        expr::const_f32(0.),
-        expr::const_f32(0.),
-        expr::load_var("w"),
-        expr::load_var("h"),
-        bg_color,
-    );
-    shape.add_filled_box(
-        expr::const_f32(0.),
-        expr::load_var("h"),
-        expr::load_var("w"),
-        cc.compile("h + 0.5").unwrap(),
-        sep_color,
-    );
-    let color1 = [0.2, 0.2, 0.2, 0.5];
-    let color2 = [0.5, 0.5, 0.5, 0.];
-    shape.add_smooth_vertical_gradient(
-        expr::const_f32(0.),
-        expr::const_f32(0.),
-        expr::load_var("w"),
-        cc.compile("h / 2").unwrap(),
-        color1,
-        color2,
-        8,
-        0.2,
-    );
-
-    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_node.link(node);
-
     // Create some text
     let node = create_text("channels_label");
     let prop = node.get_property("rect").unwrap();
@@ -352,12 +301,11 @@ pub async fn make(
     // Main button layer
     let node = create_layer("mainbtn_layer");
     let prop = node.get_property("rect").unwrap();
-    prop.set_default_f32(0, CHANNEL_LABEL_X).unwrap();
-    let code = cc.compile("h - MENU_BTN_H - CHANNEL_LABEL_X").unwrap();
+    prop.set_default_f32(0, 0.).unwrap();
+    let code = cc.compile("h - 3 * MENU_BTN_W_R - 3 * CHANNEL_LABEL_X").unwrap();
     prop.set_default_expr(1, code).unwrap();
-    let code = cc.compile("w - 2 * CHANNEL_LABEL_X").unwrap();
-    prop.set_default_expr(2, code).unwrap();
-    prop.set_default_f32(3, MENU_BTN_H).unwrap();
+    prop.set_default_expr(2, expr::load_var("w")).unwrap();
+    prop.set_default_f32(3, 3. * MENU_BTN_W_R + 2. * CHANNEL_LABEL_X).unwrap();
     node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "priority", 1).unwrap();
@@ -389,11 +337,12 @@ pub async fn make(
     let node = create_button("write_btn");
     node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
     let prop = node.get_property("rect").unwrap();
-    let code = cc.compile("w - MENU_BTN_W_R").unwrap();
+    let code = cc.compile("w - MENU_BTN_W_R - CHANNEL_LABEL_X").unwrap();
     prop.set_default_expr(0, code).unwrap();
-    prop.set_default_f32(1, 0.).unwrap();
+    let code = cc.compile("h - MENU_BTN_H").unwrap();
+    prop.set_default_expr(1, code).unwrap();
     prop.set_default_f32(2, MENU_BTN_W_R).unwrap();
-    prop.set_default_expr(3, expr::load_var("h")).unwrap();
+    prop.set_default_f32(3, MENU_BTN_H).unwrap();
     //Uncomment this to see the button outline
     //node.set_property_bool(atom, Role::App, "debug", true).unwrap();
     //node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
@@ -419,14 +368,17 @@ pub async fn make(
 
     let node = create_vector_art("write_icon");
     let prop = node.get_property("rect").unwrap();
-    let code = cc.compile("w - MENU_BTN_W_R / 2 - MENU_ICON_SCALE * 0.45").unwrap();
+    let code =
+        cc.compile("w - CHANNEL_LABEL_X - MENU_BTN_W_R / 2 - MENU_ICON_SCALE * 0.45").unwrap();
     prop.set_default_expr(0, code).unwrap();
-    prop.set_default_f32(1, MENU_BTN_H / 2. + MENU_ICON_SCALE * 0.36).unwrap();
+    let code = cc.compile("h - MENU_BTN_H / 2 + MENU_ICON_SCALE * 0.36").unwrap();
+    prop.set_default_expr(1, code).unwrap();
     prop.set_default_f32(2, MENU_ICON_SCALE).unwrap();
     prop.set_default_f32(3, MENU_ICON_SCALE).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
     let shape = shape::create_menu_icon(COLOR_CYAN).scaled(MENU_ICON_SCALE);
-    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let prop = node.get_property("shape").unwrap();
+    prop.set_default_shape(0, shape).unwrap();
     let node =
         node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     mainlayer_node.link(node);
@@ -562,11 +514,10 @@ pub async fn setup_wallet_button(app: &App, chat_layer: SceneNodePtr, i18n_fish:
     let atom = &mut PropertyAtomicGuard::none();
     let mut cc = expr::Compiler::new();
     cc.add_const_f32("MENU_BTN_W_R", MENU_BTN_W_R);
-    cc.add_const_f32("MENU_BTN_H", MENU_BTN_H);
     cc.add_const_f32("CHANNEL_LABEL_X", CHANNEL_LABEL_X);
 
     let chat_is_visible = PropertyBool::wrap(&chat_layer, Role::App, "is_visible", 0).unwrap();
-    let menu_layer = chat_layer.lookup_node("/menu_layer").unwrap();
+    let mainlayer_node = chat_layer.lookup_node("/menu_layer/mainbtn_layer").unwrap();
 
     // Wallet status icon (blockchain netlogo, all cyan = fully connected)
     // Positioned at center of wallet button area, above write_btn
@@ -574,10 +525,7 @@ pub async fn setup_wallet_button(app: &App, chat_layer: SceneNodePtr, i18n_fish:
     let prop = node.get_property("rect").unwrap();
     let code = cc.compile("w - MENU_BTN_W_R / 2 - CHANNEL_LABEL_X").unwrap();
     prop.set_default_expr(0, code).unwrap();
-    let code = cc
-        .compile("h - CHANNEL_LABEL_X - 3 * MENU_BTN_W_R - 2 * CHANNEL_LABEL_X + MENU_BTN_W_R / 2")
-        .unwrap();
-    prop.set_default_expr(1, code).unwrap();
+    prop.set_default_f32(1, MENU_BTN_W_R / 2.).unwrap();
     prop.set_default_f32(2, 1.).unwrap();
     prop.set_default_f32(3, 1.).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
@@ -586,10 +534,11 @@ pub async fn setup_wallet_button(app: &App, chat_layer: SceneNodePtr, i18n_fish:
     shape.join(shape::create_blockchain_netlogo2(COLOR_CYAN));
     shape.join(shape::create_blockchain_netlogo3(COLOR_CYAN));
     shape.join(shape::create_blockchain_netlogo4(COLOR_CYAN));
-    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let prop = node.get_property("shape").unwrap();
+    prop.set_default_shape(0, shape).unwrap();
     let node =
         node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
-    menu_layer.link(node);
+    mainlayer_node.link(node);
 
     // Wallet button (above write_btn, square: w = h = MENU_BTN_W_R)
     let node = create_button("wallet_btn");
@@ -597,8 +546,7 @@ pub async fn setup_wallet_button(app: &App, chat_layer: SceneNodePtr, i18n_fish:
     let prop = node.get_property("rect").unwrap();
     let code = cc.compile("w - MENU_BTN_W_R - CHANNEL_LABEL_X").unwrap();
     prop.set_default_expr(0, code).unwrap();
-    let code = cc.compile("h - CHANNEL_LABEL_X - 3 * MENU_BTN_W_R - 2 * CHANNEL_LABEL_X").unwrap();
-    prop.set_default_expr(1, code).unwrap();
+    prop.set_default_f32(1, 0.).unwrap();
     prop.set_default_f32(2, MENU_BTN_W_R).unwrap();
     prop.set_default_f32(3, MENU_BTN_W_R).unwrap();
     //node.set_property_bool(atom, Role::App, "debug", true).unwrap();
@@ -627,7 +575,7 @@ pub async fn setup_wallet_button(app: &App, chat_layer: SceneNodePtr, i18n_fish:
     let redraw = app.redraw_trigger.clone();
 
     let node = node.setup(|me| Button::new(me, renderer, redraw)).await;
-    menu_layer.link(node);
+    mainlayer_node.link(node);
 }
 
 pub async fn setup_settings_button(app: &App, chat_layer: SceneNodePtr) {
@@ -637,25 +585,23 @@ pub async fn setup_settings_button(app: &App, chat_layer: SceneNodePtr) {
     cc.add_const_f32("CHANNEL_LABEL_X", CHANNEL_LABEL_X);
     cc.add_const_f32("SETTINGS_ICON_SCALE", SETTINGS_ICON_SCALE);
 
-    let menu_layer = chat_layer.lookup_node("/menu_layer").unwrap();
+    let mainlayer_node = chat_layer.lookup_node("/menu_layer/mainbtn_layer").unwrap();
 
     // Settings icon
     let node = create_vector_art("settings_icon");
     let prop = node.get_property("rect").unwrap();
     let code = cc.compile("w - MENU_BTN_W_R / 2 - CHANNEL_LABEL_X").unwrap();
     prop.set_default_expr(0, code).unwrap();
-    let code = cc
-        .compile("h - CHANNEL_LABEL_X - 2 * MENU_BTN_W_R - CHANNEL_LABEL_X + MENU_BTN_W_R / 2")
-        .unwrap();
-    prop.set_default_expr(1, code).unwrap();
+    prop.set_default_f32(1, 1.5 * MENU_BTN_W_R + CHANNEL_LABEL_X).unwrap();
     prop.set_default_f32(2, SETTINGS_ICON_SCALE).unwrap();
     prop.set_default_f32(3, SETTINGS_ICON_SCALE).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
     let shape = shape::create_settings(COLOR_CYAN).scaled(SETTINGS_ICON_SCALE);
-    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let prop = node.get_property("shape").unwrap();
+    prop.set_default_shape(0, shape).unwrap();
     let node =
         node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
-    menu_layer.link(node);
+    mainlayer_node.link(node);
 
     // Settings button
     let node = create_button("settings_btn");
@@ -663,8 +609,7 @@ pub async fn setup_settings_button(app: &App, chat_layer: SceneNodePtr) {
     let prop = node.get_property("rect").unwrap();
     let code = cc.compile("w - MENU_BTN_W_R - CHANNEL_LABEL_X").unwrap();
     prop.set_default_expr(0, code).unwrap();
-    let code = cc.compile("h - CHANNEL_LABEL_X - 2 * MENU_BTN_W_R - CHANNEL_LABEL_X").unwrap();
-    prop.set_default_expr(1, code).unwrap();
+    prop.set_default_f32(1, MENU_BTN_W_R + CHANNEL_LABEL_X).unwrap();
     prop.set_default_f32(2, MENU_BTN_W_R).unwrap();
     prop.set_default_f32(3, MENU_BTN_W_R).unwrap();
     //node.set_property_bool(atom, Role::App, "debug", true).unwrap();
@@ -688,5 +633,5 @@ pub async fn setup_settings_button(app: &App, chat_layer: SceneNodePtr) {
     let redraw = app.redraw_trigger.clone();
 
     let node = node.setup(|me| Button::new(me, renderer, redraw)).await;
-    menu_layer.link(node);
+    mainlayer_node.link(node);
 }

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

@@ -46,7 +46,7 @@ pub mod test;
 pub mod test_chatview;
 pub mod test_edit;
 pub mod test_scroll_layer;
-mod wallet;
+pub mod wallet;
 
 macro_rules! i { ($($arg:tt)*) => { info!(target: "app::schema", $($arg)*); } }
 macro_rules! e { ($($arg:tt)*) => { error!(target: "app::schema", $($arg)*); } }
@@ -188,7 +188,7 @@ mod ui_consts {
 pub use ui_consts::*;
 
 pub static DEFAULT_CHANNELS: &'static [&str] =
-    &["dev", "media", "hackers", "memes", "philosophy", "markets", "math", "random"];
+    &["dev", "media", "hackers", "memes", "philosophy", "markets", "math", "random", "fit"];
 
 /// Read the ordered list of joined channels/contacts (prefixed names like "#dev", "@alice").
 pub fn read_joined_channels() -> Vec<String> {
@@ -294,6 +294,44 @@ pub async fn make(
         content.setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     window.link(content.clone());
 
+    // Shared header below screen controls and above theme backgrounds.
+    let node = create_vector_art("header_bg");
+    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_f32(3, MAIN_MENU_HEADER_HEIGHT).unwrap();
+    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., 0., 0., 1.],
+    );
+    shape.add_filled_box(
+        expr::const_f32(0.),
+        expr::load_var("h"),
+        expr::load_var("w"),
+        cc.compile("h + 0.5").unwrap(),
+        [0.41, 0.6, 0.65, 1.],
+    );
+    shape.add_smooth_vertical_gradient(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        cc.compile("h / 2").unwrap(),
+        [0.2, 0.2, 0.2, 0.5],
+        [0.5, 0.5, 0.5, 0.],
+        8,
+        0.2,
+    );
+    node.get_property("shape").unwrap().set_default_shape(0, shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
+    content.link(node);
+
     // Main menu overlay, toggled by the main_menu_btn shown on the main
     // wallet and main chat screens. Sits above both sections.
     let main_menu_layer = create_layer("main_menu_layer");
@@ -693,7 +731,7 @@ pub async fn make(
     prop.set_default_expr(2, expr::load_var("w")).unwrap();
     prop.set_default_expr(3, expr::load_var("h")).unwrap();
     chat_layer.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
-    chat_layer.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
+    chat_layer.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let chat_layer = chat_layer
         .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
         .await;

+ 1 - 40
bin/app/src/app/schema/settings.rs

@@ -206,7 +206,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     prop.set_default_expr(2, expr::load_var("w")).unwrap();
     prop.set_default_expr(3, expr::load_var("h")).unwrap();
     layer_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
-    layer_node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
+    layer_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let layer_node = layer_node
         .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
         .await;
@@ -214,45 +214,6 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
 
     let mut setting_y = 0.;
 
-    // Create the toolbar bg
-    let node = create_vector_art("toolbar_bg");
-    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_f32(3, SETTING_LABEL_LINESPACE).unwrap();
-    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
-
-    let (bg_color, sep_color) = ([0., 0.11, 0.11, 1.], [0.41, 0.6, 0.65, 1.]);
-    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"),
-        bg_color,
-    );
-    shape.add_filled_box(
-        expr::const_f32(BACKARROW_BG_W),
-        expr::const_f32(0.),
-        expr::const_f32(BACKARROW_BG_W + 1.),
-        expr::load_var("h"),
-        sep_color,
-    );
-    shape.add_filled_box(
-        expr::const_f32(0.),
-        expr::load_var("h"),
-        expr::load_var("w"),
-        cc.compile("h + 1").unwrap(),
-        sep_color,
-    );
-
-    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_node.link(node);
-
     // Create the back button
     let node = create_vector_art("back_btn_bg");
     let prop = node.get_property("rect").unwrap();

+ 0 - 2
bin/app/src/app/schema/wallet/data.rs

@@ -44,7 +44,6 @@ mod android_ui_consts {
     pub const RECIPIENT_INPUT_HEIGHT: f32 = 120.;
     pub const RECIPIENT_INPUT_FONTSIZE: f32 = 48.;
     pub const HEADER_HEIGHT: f32 = 140.;
-    pub const BACK_SEP_W: f32 = 1.;
     pub const ROW_HEIGHT: f32 = 80.;
     pub const COPY_WIDTH: f32 = 200.;
     pub const COPY_SCALE: f32 = 30.;
@@ -102,7 +101,6 @@ mod ui_consts {
     pub const RECIPIENT_INPUT_HEIGHT: f32 = 60.;
     pub const RECIPIENT_INPUT_FONTSIZE: f32 = 20.;
     pub const HEADER_HEIGHT: f32 = 60.;
-    pub const BACK_SEP_W: f32 = 0.5;
     pub const ROW_HEIGHT: f32 = 80.;
     pub const COPY_WIDTH: f32 = 100.;
     pub const COPY_SCALE: f32 = 15.;

+ 21 - 63
bin/app/src/app/schema/wallet/main.rs

@@ -24,7 +24,7 @@ use crate::{
         App,
     },
     expr,
-    gfx::{gfxtag, Point},
+    gfx::gfxtag,
     mesh::{COLOR_CYAN, COLOR_TEAL},
     prop::{PropertyAtomicGuard, PropertyBool, PropertyFloat32, Role},
     scene::{SceneNodePtr, Slot},
@@ -72,38 +72,16 @@ pub async fn make(
 
     let main_is_visible = PropertyBool::wrap(&main_layer, Role::App, "is_visible", 0).unwrap();
 
-    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_default_f32(0, 0.).unwrap();
-    prop.set_default_f32(1, 0.).unwrap();
-    prop.set_default_f32(2, BACKARROW_BG_W + BACK_SEP_W).unwrap();
-    prop.set_default_f32(3, HEADER_HEIGHT).unwrap();
+    prop.set_default_f32(0, BACKARROW_X).unwrap();
+    prop.set_default_f32(1, BACKARROW_Y).unwrap();
+    prop.set_default_f32(2, BACKARROW_SCALE).unwrap();
+    prop.set_default_f32(3, BACKARROW_SCALE).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
     node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
-    let mut shape = VectorShape::new();
-    let (_bg_color, sep_color) = ([0., 0., 0., 1.], [0.41, 0.6, 0.65, 1.]);
-    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.),
-        expr::const_f32(BACKARROW_BG_W + BACK_SEP_W),
-        expr::load_var("h"),
-        sep_color,
-    );
-    shape.join(
-        shape::create_back_arrow()
-            .scaled(BACKARROW_SCALE)
-            .offset(Point::new(BACKARROW_X, BACKARROW_Y)),
-    );
+    let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
     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;
@@ -346,7 +324,7 @@ pub async fn make(
         1.,
         COLOR_TEAL,
     );
-    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    node.get_property("shape").unwrap().set_default_shape(0, shape).unwrap();
     let node =
         node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     main_layer.link(node);
@@ -454,7 +432,7 @@ pub async fn make(
         1.,
         COLOR_TEAL,
     );
-    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    node.get_property("shape").unwrap().set_default_shape(0, shape).unwrap();
     let node =
         node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     main_layer.link(node);
@@ -531,7 +509,17 @@ pub async fn make(
     )
     .await;
 
-    create_title(app, atom, &main_layer, &window_scale, i18n_fish, "TOKENS", &mut y).await;
+    create_title(
+        app,
+        atom,
+        &main_layer,
+        &window_scale,
+        i18n_fish,
+        "tokens_label",
+        "tokens",
+        &mut y,
+    )
+    .await;
 
     let mut cc = expr::Compiler::new();
     cc.add_const_f32("PADDING_X", PADDING_X);
@@ -570,37 +558,6 @@ async fn create_chat_btn(
     cc: &expr::Compiler,
     parent: &SceneNodePtr,
 ) {
-    let node = create_vector_art("chat_btn_bg");
-    let prop = node.get_property("rect").unwrap();
-    let code = cc.compile(format!("w - {CHAT_BTN_SIZE} - {CHAT_BTN_MARGIN}")).unwrap();
-    prop.set_default_expr(0, code).unwrap();
-    let code = cc.compile(format!("h - {CHAT_BTN_SIZE} - {CHAT_BTN_MARGIN}")).unwrap();
-    prop.set_default_expr(1, code).unwrap();
-    prop.set_default_f32(2, CHAT_BTN_SIZE).unwrap();
-    prop.set_default_f32(3, CHAT_BTN_SIZE).unwrap();
-    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::load_var("h"),
-        [0., 0.098, 0.098, 1.],
-    );
-    shape.add_outline(
-        expr::const_f32(0.),
-        expr::const_f32(0.),
-        expr::load_var("w"),
-        expr::load_var("h"),
-        1.,
-        [0.2, 0.2745, 0.2784, 1.],
-    );
-    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;
-    parent.link(node);
-
     let node = create_vector_art("chat_btn_shape");
     let prop = node.get_property("rect").unwrap();
     let code = cc.compile(format!("w - {CHAT_BTN_SIZE}/2 - {CHAT_BTN_MARGIN}")).unwrap();
@@ -614,7 +571,8 @@ async fn create_chat_btn(
     let mut shape = shape::create_netlogo1([0., 0.94, 1., 1.]);
     shape.join(shape::create_netlogo2([0., 0.94, 1., 1.]));
     shape.join(shape::create_netlogo3([0., 0.94, 1., 1.]));
-    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let prop = node.get_property("shape").unwrap();
+    prop.set_default_shape(0, shape).unwrap();
     let node =
         node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     parent.link(node);

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

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

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

@@ -61,13 +61,13 @@ pub async fn make(
     create_back_shortcut(app, atom, &receive_layer).await;
 
     create_bg_mesh(app, atom, &receive_layer, "receive_bg").await;
-    create_header_bg(app, atom, &receive_layer, "receive_header_bg").await;
 
     let mut y = 0.;
 
     y += HEADER_HEIGHT;
 
-    create_title(app, atom, &receive_layer, &window_scale, i18n_fish, "RECEIVE", &mut y).await;
+    create_title(app, atom, &receive_layer, &window_scale, i18n_fish, "RECEIVE", "RECEIVE", &mut y)
+        .await;
 
     create_subtitle(
         app,

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

@@ -76,13 +76,13 @@ pub async fn make(
         PropertyBool::wrap(&send_step1_layer, Role::App, "is_visible", 0).unwrap();
 
     create_bg_mesh(app, atom, &send_step1_layer, "send_bg").await;
-    create_header_bg(app, atom, &send_step1_layer, "send_header_bg").await;
 
     let mut y = 0.;
 
     y += HEADER_HEIGHT;
 
-    create_title(app, atom, &send_step1_layer, &window_scale, i18n_fish, "SEND", &mut y).await;
+    create_title(app, atom, &send_step1_layer, &window_scale, i18n_fish, "SEND", "SEND", &mut y)
+        .await;
 
     create_subtitle(
         app,

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

@@ -107,13 +107,13 @@ pub async fn make(
         PropertyBool::wrap(&send_step2_layer, Role::App, "is_visible", 0).unwrap();
 
     create_bg_mesh(app, atom, &send_step2_layer, "send_bg2").await;
-    create_header_bg(app, atom, &send_step2_layer, "send_header_bg2").await;
 
     let mut y = 0.;
 
     y += HEADER_HEIGHT;
 
-    create_title(app, atom, &send_step2_layer, &window_scale, i18n_fish, "SEND", &mut y).await;
+    create_title(app, atom, &send_step2_layer, &window_scale, i18n_fish, "SEND", "SEND", &mut y)
+        .await;
 
     // Selected token display
     let node = create_text("send_selected_token_symbol");

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

@@ -78,13 +78,13 @@ pub async fn make(
         PropertyBool::wrap(&send_step3_layer, Role::App, "is_visible", 0).unwrap();
 
     create_bg_mesh(app, atom, &send_step3_layer, "send_bg3").await;
-    create_header_bg(app, atom, &send_step3_layer, "send_header_bg3").await;
 
     let mut y = 0.;
 
     y += HEADER_HEIGHT;
 
-    create_title(app, atom, &send_step3_layer, &window_scale, i18n_fish, "SEND", &mut y).await;
+    create_title(app, atom, &send_step3_layer, &window_scale, i18n_fish, "SEND", "SEND", &mut y)
+        .await;
 
     // Selected token display (same layout as step2)
     let node = create_text("send_selected_token_symbol3");

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

@@ -76,13 +76,13 @@ pub async fn make(
         PropertyBool::wrap(&send_step4_layer, Role::App, "is_visible", 0).unwrap();
 
     create_bg_mesh(app, atom, &send_step4_layer, "send_bg4").await;
-    create_header_bg(app, atom, &send_step4_layer, "send_header_bg4").await;
 
     let mut y = 0.;
 
     y += HEADER_HEIGHT;
 
-    create_title(app, atom, &send_step4_layer, &window_scale, i18n_fish, "SEND", &mut y).await;
+    create_title(app, atom, &send_step4_layer, &window_scale, i18n_fish, "SEND", "SEND", &mut y)
+        .await;
 
     // Selected token display
     let node = create_text("send_selected_token_symbol4");

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

@@ -116,11 +116,11 @@ pub async fn make(
         PropertyBool::wrap(&tx_status_layer, Role::App, "is_visible", 0).unwrap();
 
     create_bg_mesh(app, atom, &tx_status_layer, "tx_status_bg").await;
-    create_header_bg(app, atom, &tx_status_layer, "tx_status_header_bg").await;
 
     let mut y = HEADER_HEIGHT;
 
-    create_title(app, atom, &tx_status_layer, &window_scale, i18n_fish, "SEND", &mut y).await;
+    create_title(app, atom, &tx_status_layer, &window_scale, i18n_fish, "SEND", "SEND", &mut y)
+        .await;
 
     // Status text
     let node = create_text("status");

+ 7 - 56
bin/app/src/app/schema/wallet/util.rs

@@ -148,6 +148,7 @@ pub async fn create_title(
     window_scale: &PropertyFloat32,
     i18n_fish: &I18nBabelFish,
     name: &str,
+    text: &str,
     y: &mut f32,
 ) -> SceneNodePtr {
     let node = create_text(name);
@@ -157,7 +158,7 @@ pub async fn create_title(
     prop.set_default_f32(2, 1000.).unwrap();
     prop.set_default_f32(3, TITLE_FONTSIZE).unwrap();
     node.get_property("font_size").unwrap().set_default_f32(0, TITLE_FONTSIZE).unwrap();
-    node.set_property_str(atom, Role::App, "text", name).unwrap();
+    node.set_property_str(atom, Role::App, "text", text).unwrap();
     let prop = node.get_property("text_color").unwrap();
     prop.set_default_f32_multi(&[1., 1., 1., 1.]).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
@@ -181,7 +182,7 @@ pub async fn create_title(
         &app.redraw_trigger,
         atom,
         layer,
-        &format!("{}_separator", name),
+        &format!("{name}_separator"),
         y,
     )
     .await;
@@ -239,7 +240,7 @@ pub async fn create_subtitle(
     node
 }
 
-/// Creates a background mesh with gradient box.
+/// Creates a background mesh below the shared header and its separator.
 pub async fn create_bg_mesh(
     app: &App,
     atom: &mut PropertyAtomicGuard,
@@ -249,9 +250,10 @@ pub async fn create_bg_mesh(
     let node = create_vector_art(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_f32(1, HEADER_HEIGHT + 1.).unwrap();
     prop.set_default_expr(2, expr::load_var("w")).unwrap();
-    prop.set_default_expr(3, expr::load_var("h")).unwrap();
+    let cc = Compiler::new();
+    prop.set_default_expr(3, cc.compile(format!("h - {HEADER_HEIGHT} - 1")).unwrap()).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
     let mut shape = VectorShape::new();
     shape.add_gradient_box(
@@ -267,57 +269,6 @@ pub async fn create_bg_mesh(
     layer.link(node);
 }
 
-/// Creates a header background with filled box and separator line at bottom.
-pub async fn create_header_bg(
-    app: &App,
-    atom: &mut PropertyAtomicGuard,
-    layer: &SceneNodePtr,
-    name: &str,
-) {
-    let node = create_vector_art(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_f32(3, HEADER_HEIGHT).unwrap();
-    node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
-
-    let (bg_color, sep_color) = ([0., 0., 0., 1.], [0.41, 0.6, 0.65, 1.]);
-
-    let cc = Compiler::new();
-    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"),
-        bg_color,
-    );
-    shape.add_filled_box(
-        expr::const_f32(0.),
-        expr::load_var("h"),
-        expr::load_var("w"),
-        cc.compile("h + 0.5").unwrap(),
-        sep_color,
-    );
-    let color1 = [0.2, 0.2, 0.2, 0.5];
-    let color2 = [0.5, 0.5, 0.5, 0.];
-    shape.add_smooth_vertical_gradient(
-        expr::const_f32(0.),
-        expr::const_f32(0.),
-        expr::load_var("w"),
-        cc.compile("h / 2").unwrap(),
-        color1,
-        color2,
-        8,
-        0.2,
-    );
-    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);
-}
-
 /// Creates a separator line at the given y expression.
 /// Returns the separator node after setup, linked to the layer
 pub async fn create_separator_expr(

+ 1 - 1
bin/app/src/mesh.rs

@@ -64,7 +64,7 @@ pub const COLOR_PURPLE: Color = [1., 0., 1., 1.];
 #[allow(dead_code)]
 pub const COLOR_WHITE: Color = [1., 1., 1., 1.];
 #[allow(dead_code)]
-pub const COLOR_BLACK: Color = [1., 1., 1., 1.];
+pub const COLOR_BLACK: Color = [0., 0., 0., 1.];
 #[allow(dead_code)]
 pub const COLOR_GREY: Color = [0.5, 0.5, 0.5, 1.];
 #[allow(dead_code)]

+ 1 - 1
bin/app/src/scene.rs

@@ -46,7 +46,7 @@ pub struct ScenePath(VecDeque<String>);
 impl<S: Into<String>> From<S> for ScenePath {
     fn from(path: S) -> Self {
         let path: String = path.into();
-        (&path).parse().expect("invalid ScenePath &str")
+        (&path).parse().expect(&format!("invalid ScenePath {path}"))
     }
 }
 

+ 208 - 0
bin/app/src/theme/light.rs

@@ -0,0 +1,208 @@
+/* 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/>.
+ */
+
+//! The `light` theme: the `minimal` palette with every color token
+//! inverted (each RGB component mapped to `1 - v`, alpha preserved),
+//! plus a full-screen white `king` vector-art background at the lowest
+//! z band — the light analog of scifi's king video.
+//!
+//! Unload restores the minimal defaults by the standard journal replay
+//! and unlinks the king node.
+
+use std::sync::Arc;
+
+use crate::{
+    app::{
+        node::create_vector_art,
+        schema::menu::ui_consts::{MENU_ICON_SCALE, SETTINGS_ICON_SCALE},
+    },
+    error::{Error, Result},
+    expr,
+    mesh::{rgba, COLOR_BLACK},
+    prop::{PropertyAtomicGuard, PropertyValue, Role},
+    shape,
+    theme::{Theme, ThemeCtx},
+    ui::{VectorArt, VectorShape},
+};
+
+pub struct LightTheme;
+
+#[async_trait::async_trait]
+impl Theme for LightTheme {
+    fn name(&self) -> &'static str {
+        "light"
+    }
+
+    async fn apply(&self, ctx: &ThemeCtx) -> Result<()> {
+        light_apply(ctx).await
+    }
+}
+
+async fn light_apply(ctx: &ThemeCtx) -> Result<()> {
+    let atom = &mut PropertyAtomicGuard::none();
+    let cc = expr::Compiler::new();
+
+    let tokens: &[(&str, [f32; 4])] = &[
+        ("text_color", [0.08, 0.08, 0.08, 1.]),
+        ("edit.text_color", [0.08, 0.08, 0.08, 1.]),
+        ("edit.hi_bg_color", [0.65, 0.65, 0.65, 1.]),
+        ("edit.text_hi_color", [1., 1., 1., 1.]),
+        ("edit.cursor_color", [0.10, 0.10, 0.10, 1.]),
+        ("edit.action_fg_color", [0.10, 0.10, 0.10, 1.]),
+        ("edit.action_bg_color", [0.85, 0.85, 0.85, 1.]),
+        ("menu.bg_color", [0.95, 0.95, 0.95, 0.5]),
+        ("menu.role1_color", [0.40, 0.40, 0.40, 1.]),
+        ("menu.role2_color", [0.25, 0.25, 0.25, 1.]),
+        ("chatview.timestamp_color", [0.45, 0.45, 0.45, 1.]),
+        ("chatview.text_color", [0.08, 0.08, 0.08, 1.]),
+        ("chatview.hi_bg_color", [0.75, 0.75, 0.75, 1.]),
+        ("chatview.action_text_color", [0.20, 0.20, 0.20, 1.]),
+        ("chatview.url_text_color", [0.30, 0.20, 0.10, 1.]),
+    ];
+    for (name, val) in tokens {
+        ctx.set_shared_token_color(atom, name, *val)?;
+    }
+
+    king_vectorart_node(ctx).await;
+
+    let sg_root = ctx.sg_root();
+
+    let node = sg_root.lookup_node("/window/content/wallet/main_layer/chat_btn_shape").unwrap();
+    let color = rgba!(0xF41740ff);
+    let mut shape = shape::create_netlogo1(color);
+    shape.join(shape::create_netlogo2(color));
+    shape.join(shape::create_netlogo3(color));
+    let prop = node.get_property("shape").unwrap();
+    ctx.set_touched(atom, &prop, 0, PropertyValue::VectorShape(Arc::new(shape)))?;
+
+    let node =
+        sg_root.lookup_node("/window/content/chat/menu_layer/mainbtn_layer/wallet_icon").unwrap();
+    let mut shape = shape::create_blockchain_netlogo1(color);
+    shape.join(shape::create_blockchain_netlogo2(color));
+    shape.join(shape::create_blockchain_netlogo3(color));
+    shape.join(shape::create_blockchain_netlogo4(color));
+    let prop = node.get_property("shape").unwrap();
+    ctx.set_touched(atom, &prop, 0, PropertyValue::VectorShape(Arc::new(shape)))?;
+
+    let node =
+        sg_root.lookup_node("/window/content/chat/menu_layer/mainbtn_layer/settings_icon").unwrap();
+    let shape = shape::create_settings(color).scaled(SETTINGS_ICON_SCALE);
+    let prop = node.get_property("shape").unwrap();
+    ctx.set_touched(atom, &prop, 0, PropertyValue::VectorShape(Arc::new(shape)))?;
+
+    let node =
+        sg_root.lookup_node("/window/content/chat/menu_layer/mainbtn_layer/write_icon").unwrap();
+    let shape = shape::create_menu_icon(color).scaled(MENU_ICON_SCALE);
+    let prop = node.get_property("shape").unwrap();
+    ctx.set_touched(atom, &prop, 0, PropertyValue::VectorShape(Arc::new(shape)))?;
+
+    let color = rgba!(0x3300ffff);
+    let node = sg_root.lookup_node("/window/content/wallet/main_layer/receive_label").unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    ctx.set_touched_color(atom, &prop, color)?;
+    let node = sg_root.lookup_node("/window/content/wallet/main_layer/receive_btn_bg").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,
+    );
+    let prop = node.get_property("shape").unwrap();
+    ctx.set_touched(atom, &prop, 0, PropertyValue::VectorShape(Arc::new(shape)))?;
+    let node = sg_root.lookup_node("/window/content/wallet/main_layer/wallet_send_btn_bg").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,
+    );
+    let prop = node.get_property("shape").unwrap();
+    ctx.set_touched(atom, &prop, 0, PropertyValue::VectorShape(Arc::new(shape)))?;
+    let node = sg_root.lookup_node("/window/content/wallet/main_layer/wallet_send_label").unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    ctx.set_touched_color(atom, &prop, color)?;
+    let node = sg_root.lookup_node("/window/content/wallet/main_layer/tokens_label").unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    ctx.set_touched_color(atom, &prop, COLOR_BLACK)?;
+    let node = sg_root.lookup_node("/window/content/wallet/main_layer/tokens_table").unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    ctx.set_touched_color(atom, &prop, COLOR_BLACK)?;
+
+    let node = sg_root.lookup_node("/window/content/header_bg").unwrap();
+    let mut shape = VectorShape::new();
+    shape.add_filled_box(
+        expr::const_f32(0.),
+        expr::load_var("h"),
+        expr::load_var("w"),
+        cc.compile("h + 1").unwrap(),
+        COLOR_BLACK,
+    );
+    let color1 = rgba!(0x0000ffff);
+    let color2 = rgba!(0x35ffffff);
+    shape.add_smooth_vertical_gradient(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        color1,
+        color2,
+        8,
+        0.2,
+    );
+    let prop = node.get_property("shape").unwrap();
+    ctx.set_touched(atom, &prop, 0, PropertyValue::VectorShape(Arc::new(shape)))?;
+
+    Ok(())
+}
+
+/// The full-screen white king background, as a tracked structural node
+/// in the reserved low z band under `/window/content`.
+async fn king_vectorart_node(ctx: &ThemeCtx) {
+    let app = ctx.app().unwrap();
+    let content = ctx.sg_root().lookup_node("/window/content").unwrap();
+
+    let node = create_vector_art("king");
+    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();
+
+    let color_white = rgba!(0xF6FAFFff);
+    let color_blue = rgba!(0xC3D8FFff);
+
+    let mut shape = VectorShape::new();
+    shape.add_gradient_box(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        [color_white, color_white, color_blue, color_blue],
+    );
+    node.set_property_shape(&mut PropertyAtomicGuard::none(), Role::App, "shape", shape).unwrap();
+    node.set_property_u32(&mut PropertyAtomicGuard::none(), Role::App, "z_index", 0).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
+    ctx.link_tracked(&content, node);
+}

+ 49 - 11
bin/app/src/theme/scifi.rs

@@ -31,7 +31,10 @@ use indoc::indoc;
 use crate::{
     app::{
         node::{create_vector_art, create_video},
-        schema::{VID_ASPECT_RATIO, VID_PATH},
+        schema::{
+            wallet::data::{CHAT_BTN_MARGIN, CHAT_BTN_SIZE},
+            MAIN_MENU_HEADER_HEIGHT, VID_ASPECT_RATIO, VID_PATH,
+        },
     },
     error::{Error, Result},
     expr::{self, Compiler},
@@ -75,9 +78,11 @@ fn private_token_props() -> Vec<Property> {
 }
 
 async fn scifi_apply(ctx: &ThemeCtx) -> Result<()> {
-    let Some(app) = ctx.app() else { return Err(Error::ThemeNotFound) };
+    let app = ctx.app().unwrap();
     let atom = &mut PropertyAtomicGuard::none();
 
+    let cc = Compiler::new();
+
     // ------------------------------------------------------------------
     // 1. Shared token values: the cyan palette over the minimal baseline
     // ------------------------------------------------------------------
@@ -152,10 +157,43 @@ async fn scifi_apply(ctx: &ThemeCtx) -> Result<()> {
     //    z band under /window/content) plus the full-screen fade meshes
     //    darkening it behind the chat and wallet screens
     // ------------------------------------------------------------------
-    king_video_node(ctx).await?;
+    king_video_node(ctx).await;
     bg_fade_node(ctx, "/window/content/chat/main_chat_layer", "bg").await?;
     bg_fade_node(ctx, "/window/content/wallet/main_layer", "wallet_bg").await?;
 
+    // Bg for wallet btn to navigate to chat screen
+    let node = create_vector_art("chat_btn_bg");
+    let prop = node.get_property("rect").unwrap();
+    let code = cc.compile(format!("w - {CHAT_BTN_SIZE} - {CHAT_BTN_MARGIN}")).unwrap();
+    prop.set_default_expr(0, code).unwrap();
+    let code = cc.compile(format!("h - {CHAT_BTN_SIZE} - {CHAT_BTN_MARGIN}")).unwrap();
+    prop.set_default_expr(1, code).unwrap();
+    prop.set_default_f32(2, CHAT_BTN_SIZE).unwrap();
+    prop.set_default_f32(3, CHAT_BTN_SIZE).unwrap();
+    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::load_var("h"),
+        [0., 0.098, 0.098, 1.],
+    );
+    shape.add_outline(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        1.,
+        [0.2, 0.2745, 0.2784, 1.],
+    );
+    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;
+    let parent = ctx.sg_root().lookup_node("/window/content/wallet/main_layer").unwrap();
+    ctx.link_tracked(&parent, node);
+
     // ------------------------------------------------------------------
     // 5. First-run scramble splash (theme content, design D12)
     // ------------------------------------------------------------------
@@ -208,9 +246,9 @@ async fn scifi_apply(ctx: &ThemeCtx) -> Result<()> {
 }
 
 /// The king video background, as a tracked structural node.
-async fn king_video_node(ctx: &ThemeCtx) -> Result<()> {
-    let Some(app) = ctx.app() else { return Err(Error::ThemeNotFound) };
-    let content = ctx.sg_root().lookup_node("/window/content").ok_or(Error::NodeNotFound)?;
+async fn king_video_node(ctx: &ThemeCtx) {
+    let app = ctx.app().unwrap();
+    let content = ctx.sg_root().lookup_node("/window/content").unwrap();
 
     let mut cc = Compiler::new();
     let node = create_video("king");
@@ -255,11 +293,9 @@ async fn king_video_node(ctx: &ThemeCtx) -> Result<()> {
         })
         .await;
     ctx.link_tracked(&content, node);
-    Ok(())
 }
 
-/// The full-screen fade mesh darkening the king video behind a screen
-/// layer, as a tracked structural node in the reserved low z band.
+/// The fade mesh below the shared header, darkening the king video behind a screen.
 async fn bg_fade_node(ctx: &ThemeCtx, parent_path: &str, name: &str) -> Result<()> {
     let Some(app) = ctx.app() else { return Err(Error::ThemeNotFound) };
     let parent = ctx.sg_root().lookup_node(parent_path).ok_or(Error::NodeNotFound)?;
@@ -267,9 +303,11 @@ async fn bg_fade_node(ctx: &ThemeCtx, parent_path: &str, name: &str) -> Result<(
     let node = create_vector_art(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_f32(1, MAIN_MENU_HEADER_HEIGHT + 1.).unwrap();
     prop.set_default_expr(2, expr::load_var("w")).unwrap();
-    prop.set_default_expr(3, expr::load_var("h")).unwrap();
+    let cc = Compiler::new();
+    prop.set_default_expr(3, cc.compile(format!("h - {MAIN_MENU_HEADER_HEIGHT} - 1")).unwrap())
+        .unwrap();
     node.set_property_u32(&mut PropertyAtomicGuard::none(), Role::App, "z_index", 0).unwrap();
     let mut shape = VectorShape::new();
     shape.add_gradient_box(

+ 49 - 49
bin/app/src/ui/emoji_picker/default.rs

@@ -17,55 +17,55 @@
  */
 
 pub static DEFAULT_EMOJI_LIST: &[&str] = &[
-    //"\u{f0000}",
-    //"\u{f0001}",
-    //"\u{f0002}",
-    //"\u{f0003}",
-    //"\u{f0004}",
-    //"\u{f0005}",
-    //"\u{f0006}",
-    //"\u{f0007}",
-    //"\u{f0008}",
-    //"\u{f0009}",
-    //"\u{f000a}",
-    //"\u{f000b}",
-    //"\u{f000c}",
-    //"\u{f000d}",
-    //"\u{f000e}",
-    //"\u{f000f}",
-    //"\u{f0010}",
-    //"\u{f0011}",
-    //"\u{f0012}",
-    //"\u{f0013}",
-    //"\u{f0014}",
-    //"\u{f0015}",
-    //"\u{f0016}",
-    //"\u{f0017}",
-    //"\u{f0018}",
-    //"\u{f0019}",
-    //"\u{f001a}",
-    //"\u{f001b}",
-    //"\u{f001c}",
-    //"\u{f001d}",
-    //"\u{f001e}",
-    //"\u{f001f}",
-    //"\u{f0020}",
-    //"\u{f0021}",
-    //"\u{f0022}",
-    //"\u{f0023}",
-    //"\u{f0024}",
-    //"\u{f0025}",
-    //"\u{f0026}",
-    //"\u{f0027}",
-    //"\u{f0028}",
-    //"\u{f0029}",
-    //"\u{f002a}",
-    //"\u{f002b}",
-    //"\u{f002c}",
-    //"\u{f002d}",
-    //"\u{f002e}",
-    //"\u{f002f}",
-    //"\u{f0030}",
+    "\u{f0000}",
+    "\u{f0001}",
+    "\u{f0002}",
+    "\u{f0003}",
+    "\u{f0004}",
+    "\u{f0005}",
+    "\u{f0006}",
+    "\u{f0007}",
+    "\u{f0008}",
+    "\u{f0009}",
+    "\u{f000a}",
+    "\u{f000b}",
+    "\u{f000c}",
+    "\u{f000d}",
+    "\u{f000e}",
+    "\u{f000f}",
+    "\u{f0010}",
+    "\u{f0011}",
+    "\u{f0012}",
+    "\u{f0013}",
+    "\u{f0014}",
+    "\u{f0015}",
+    "\u{f0016}",
+    "\u{f0017}",
+    "\u{f0018}",
+    "\u{f0019}",
+    "\u{f001a}",
+    "\u{f001b}",
+    "\u{f001c}",
+    "\u{f001d}",
+    "\u{f001e}",
+    "\u{f001f}",
+    "\u{f0020}",
+    "\u{f0021}",
+    "\u{f0022}",
+    "\u{f0023}",
+    "\u{f0024}",
+    "\u{f0025}",
+    "\u{f0026}",
+    "\u{f0027}",
+    "\u{f0028}",
+    "\u{f0029}",
+    "\u{f002a}",
+    "\u{f002b}",
+    "\u{f002c}",
+    "\u{f002d}",
+    "\u{f002e}",
+    "\u{f002f}",
+    "\u{f0030}",
     "😂",
     "❤️",
     "😁",

+ 0 - 2
openspec/changes/app-theme/.openspec.yaml

@@ -1,2 +0,0 @@
-schema: spec-driven
-created: 2026-08-31

+ 0 - 821
openspec/changes/app-theme/design.md

@@ -1,821 +0,0 @@
-## Context
-
-See proposal.md for motivation. The machinery this builds on, verified
-against the code:
-
-- `Property` (`src/prop/mod.rs`) resolves reads in layers: `vals[i]` →
-  if it is an expression, `cache[i]` (else `defaults[i]`) → if unset,
-  `defaults[i]` → type default. `defaults` is `Vec<PropertyValue>` and
-  `PropertyValue::SExpr` exists, but defaults are only settable pre-`Arc`
-  via builder `set_defaults_*` (no expression variant), so schema code
-  can only write `vals`. `allow_exprs()` is also builder-only: a live
-  property can never gain expr support, so factories must opt in.
-- Exactly one evaluator exists: `PropertyRect::eval_with`
-  (`src/prop/wrap.rs:377`) — gathers globals from `add_depend` edges plus
-  extras (parent w/h), runs the s-expr machine per expr index, writes the
-  cache. Widgets run it in their per-pass paths (`MultiLine::eval_rect`,
-  etc.). Colors and other styled f32 props are always plain values today.
-- Batching (`src/prop/guard.rs`): a `PropertyAtomicGuard` collects
-  `(prop, role, action)` and on **Drop** notifies all subscribers with a
-  shared `BatchGuard`. `RedrawTrigger::make_guard` (`src/ui/mod.rs:136`)
-  binds the batch's `end_batch` to exactly one redraw token, fired when
-  the last `BatchGuard` reference drops; `trigger()` enqueues into a
-  bounded(1) channel, so multiple triggers coalesce into one pass.
-- Pubsub (`src/pubsub.rs`): `Publisher::notify` is a non-blocking
-  `try_send` into **unbounded** per-subscriber queues — events published
-  before a listener task starts polling are buffered and drained later.
-  Therefore applying a theme during `App::setup` (before widget listener
-  tasks exist in `App::start`) is safe: nothing is lost.
-- Widgets (`src/ui/text.rs:80-90, 231-264`) wrap their properties with
-  `Role::Internal` and use `when_change_external` handlers that only
-  clear the widget's draw cache and request a pass; the handler skips
-  `Role::Internal` echoes so draw-pass evaluations don't retrigger
-  passes forever. Evaluation belongs in the draw path; invalidation
-  belongs in handlers.
-- Node/task teardown precedent: `menu/mod.rs:486-509` channel deletion
-  does `clear_tasks()` + `unlink()` + `redraw.trigger()`.
-- Settings: `/setting` props persist via the `Setting` pimpl
-  (`src/setting.rs`); `Setting::new` loads persisted rows with
-  `get_property(&name).unwrap()` — an unknown persisted key panics.
-  Enums render as "unknown" in the settings screen. Builder enum
-  defaults are written as `PropertyValue::Str` (`set_defaults_str` on an
-  Enum property), a latent type mismatch.
-- Styling is inline across `src/app/schema/**` (23 files); the scifi
-  palette leaks into `src/app/node.rs` factories;
-  `COLOR_SCHEME`/`PaperLight` is a compile-time second look whose arms
-  are dead (`const DarkMode`).
-
-## Goals / Non-Goals
-
-**Goals:**
-
-- Layered theming that reuses property fallback semantics: baseline =
-  defaults, theme = `vals`, unload = unset.
-- Theme switching that is O(tokens), atomic, and covers nodes created at
-  runtime (no re-theming pass, no `on_link` signal needed).
-- A defined style/structure split: schema owns layout tree and functional
-  wiring; themes own styling, decorations, and reactions.
-
-**Non-Goals:**
-
-- Making `VectorShape` internal colors expression-driven (shape colors
-  are baked consts; scifi rebuilds themed shapes structurally — revisit
-  later if a theme needs it).
-- Dynamic/runtime-loaded themes; the registry is compile-time
-  (`minimal`, `scifi`).
-- Theming plugin-owned UI beyond what plugins inherit from shared tokens.
-- A themes-from-disk or remote theme format.
-
-## Decisions
-
-### D1: Layer on defaults/vals instead of rebuilding or walking
-
-Switching rebuilds nothing. Minimal installs defaults; themes set `vals`;
-unload unsets. Alternatives considered: (a) destroy and re-run
-`schema::make` per switch — loses runtime state (scroll, focus, joined
-channels wiring) and is O(everything); (b) walk the tree setting props —
-no `on_link` signal exists, so nodes created after a switch (new channel
-screens) would be missed, and unload needs a journal of everything.
-Defaults/vals gives restore semantics for free and pushes the dynamic-node
-problem into creation-time wiring (D5).
-
-### D2: Post-creation default installation, no modify events
-
-New methods on `PropertyPtr` (`src/prop/mod.rs`), mutating `defaults[i]`
-with the same type/length checks as the builder variants:
-
-```rust
-impl Property {
-    pub fn set_default_bool(&self, i: usize, val: bool) -> Result<()>;
-    pub fn set_default_u32(&self, i: usize, val: u32) -> Result<()>;
-    pub fn set_default_f32(&self, i: usize, val: f32) -> Result<()>;
-    pub fn set_default_f32_multi(&self, vals: &[f32]) -> Result<()>;
-    pub fn set_default_str<S: Into<String>>(&self, i: usize, val: S) -> Result<()>;
-    /// Writes PropertyValue::Enum (unlike builder set_defaults_str, which
-    /// writes Str onto Enum properties — a latent type mismatch).
-    pub fn set_default_enum<S: Into<String>>(&self, i: usize, val: S) -> Result<()>;
-    pub fn set_default_expr(&self, i: usize, code: SExprCode) -> Result<()>;
-    /// Raw variant used by the journal and token node construction.
-    pub fn set_default_value(&self, i: usize, val: PropertyValue) -> Result<()>;
-}
-```
-
-Installing a default emits **no** modification event: it is a
-construction-time operation (before first frame) or happens inside a
-switch batch where the accompanying unsets already notify. Rule:
-defaults are never mutated as a live styling mechanism. Alternative — a
-new `ModifyAction` for default changes — adds pubsub surface for no
-current consumer; revisit if a future feature needs live default editing.
-
-### D3: SExpr defaults with effective-source semantics; single shared cache
-
-`is_expr`/`get_expr` resolve the **effective expression**: the `vals`
-expression if present, else the `defaults` expression. `get_value`
-resolution per the `prop-defaults` spec — never returns an unresolved
-expression:
-
-```rust
-pub fn get_value(&self, i: usize) -> Result<PropertyValue> {
-    match self.get_raw_value(i)? {
-        PropertyValue::SExpr(_) => {
-            let cached = self.get_cached(i)?;
-            if !cached.is_null() { return Ok(cached) }
-            // fall through to the default layer
-            self.default_or_type_default(i)
-        }
-        PropertyValue::Unset => Ok(self.default_or_type_default(i)),
-        v => Ok(v),
-    }
-}
-
-/// default layer: plain default → default-expr cache → type default
-fn default_or_type_default(&self, i: usize) -> PropertyValue {
-    match &self.defaults[i] {
-        PropertyValue::SExpr(_) => match self.get_cached(i) {
-            Ok(c) if !c.is_null() => c,
-            _ => self.typ.default_value(),
-        },
-        d => d.clone(),
-    }
-}
-```
-
-One cache per index is shared between the two expression sources — after
-a source switch the cache is stale until the next evaluation. With
-draw-side evaluation (D7) **every draw pass recomputes expr indices from
-current dependency values**, so no pass can observe a stale cache: the
-window closes at the next pass, which is exactly the pass the switch
-triggers. Two separate caches would double the state for zero observable
-gain. Alternative rejected.
-
-### D4: Themes may override any property, including rect; ownership rule
-
-Theme overrides are plain `vals` (value or expression) over baseline
-default-exprs; unset restores. Exception — properties the owning widget
-itself writes at runtime (e.g. multiline edit writes `rect[3]` height via
-`Role::Internal` in `eval_rect`, `src/ui/edit/behave.rs:140`) are
-last-writer-wins: themes MUST NOT override them, or the widget clobbers
-the theme. This rule is **enforced** by D13: widget-written properties
-carry write masks without `Theme`, so a theme attempt fails with
-`PropertyPermissionDenied` instead of silently losing a write race. The
-5.1 scrub assigns the masks; the D4 audit becomes that assignment.
-
-### D5: Live dependency rewiring with automatic listener resync
-
-`when_change` subscriptions snapshot `get_depends()` at widget
-construction and the poll loop rebuilds its poll set from that list
-every iteration (`ui/mod.rs:246-312`) — the list is only fixed because
-it is captured by value. Instead of forbidding later wiring, make the
-depends list a live thing:
-
-1. `Property` (`src/prop/mod.rs`) gains a depends-changed publisher and
-   a remover; `add_depend` notifies:
-
-```rust
-depends_pub: Publisher<()>,   // new field, beside on_modify
-
-pub fn add_depend<S: Into<String>>(&self, prop: &PropertyPtr, i: usize, local_name: S) {
-    self.depends.lock().unwrap().push(PropertyDepend { /* … */ });
-    self.depends_pub.notify(());
-}
-
-/// Remove edges matching (dep prop, index, local name) — theme unload
-/// restores original wiring with it.
-pub fn remove_depend(&self, prop: &PropertyPtr, i: usize, local_name: &str);
-```
-
-2. `when_change_impl` (`src/ui/mod.rs`) shares the subscription list in
-   an `Arc<Mutex<Vec<_>>>`, subscribes to `prop.depends_pub`, and on
-   receipt rebuilds the dependency entries from a fresh `get_depends()`
-   snapshot (entry 0 — the property itself — never changes), then runs
-   the handler once more: dropping a receiver discards its queued
-   messages, so the extra run closes any event missed during the swap.
-   It is the existing invalidate+trigger handler, coalesced by the
-   bounded(1) redraw channel.
-
-Theme rules under this mechanism:
-
-- Theme expressions may reference **any** property (not just
-  creation-wired names) by adding depends at apply time; the resync
-   makes the widget hear them. D4 still applies: never on properties
-  the owning widget writes at runtime.
-- Theme-added edges are journaled (D8) and removed on unload, so
-  repeated switches don't accumulate stale edges.
-- Theme-local names must be fresh (convention: prefixed, e.g.
-  `th_*`) — a duplicate local name would shadow another in the eval
-  globals.
-- Startup ordering converges either way: if the theme applies before
-  widget tasks start, the construction snapshot already includes the
-  edges; if after, the buffered depends-changed event triggers resync.
-
-The explicit `stop()`/`start()` walk (`win/mod.rs:276-291`) remains as a
-coarse re-init fallback (rebuilds every handler of a widget); the theme
-engine does not need it.
-
-Alternatives considered: (a) creation-only wiring — rejected: too
-restrictive for theme expressions; (b) explicit stop()/start() as the
-primary mechanism — rejected: coarse (whole widget), async churn, and
-the engine would have to locate affected widgets; kept as fallback.
-
-### D6: `/theme` token node + wiring helpers ("classes")
-
-A root-level `/theme` node (sibling of `/setting`) created **before**
-`schema::make`, holding the themeable vocabulary as token properties:
-colors as 4×f32 (`PropertySubType::Color`), sizes/spacing as f32. Token
-defaults = minimal palette, installed at node construction (D2, no
-events). Helpers replace inline styling blocks in schema code:
-
-```rust
-// src/theme/mod.rs (sketch)
-/// Wire `prop_name` on `node` so each component follows the token
-/// `token_name` on the /theme node. Installs default-exprs + depends.
-/// Replaces the 4× set_f32 styling blocks used across the schema.
-pub fn wire_color(
-    node: &SceneNodePtr,
-    prop_name: &str,
-    theme: &SceneNodePtr,
-    token_name: &str,
-) -> Result<()> {
-    let prop = node.get_property(prop_name).ok_or(Error::PropertyNotFound)?;
-    let token = theme.get_property(token_name).ok_or(Error::PropertyNotFound)?;
-    for i in 0..4 {
-        let local = format!("{token_name}_{i}");
-        prop.set_default_expr(i, expr::load_var(&local))?;
-        prop.add_depend(&token, i, local);
-    }
-    Ok(())
-}
-
-/// Single-f32 variant for font_size, padding, spacing, etc.
-pub fn wire_f32(
-    node: &SceneNodePtr,
-    prop_name: &str,
-    theme: &SceneNodePtr,
-    token_name: &str,
-) -> Result<()> { /* same, index 0 */ }
-```
-
-The token is the class; the helper call is the class assignment. Theme
-apply = set `vals` on ~30 tokens. **Prerequisite**: `allow_exprs()` is
-builder-only, so every factory in `src/app/node.rs` must call it for
-every themeable prop (`text_color`, `*_color`, `font_size`, `padding`,
-`lineheight`, spacing…) — otherwise `set_default_expr`/`set_expr` fail
-with `PropertySExprNotAllowed`. This factory pass happens with the Tier-0
-scrub (tasks 5.1).
-
-**Theme-defined tokens.** The `/theme` root carries only the *shared*
-vocabulary — the tokens the schema wires defaults to, present in every
-theme, forming the minimal palette. Themes are not boxed in by it:
-`add_property` is builder-only (`&mut self` on an owned node), so live
-properties cannot be appended to the linked `/theme` node — but child
-nodes link to live parents freely (that is how the whole schema is
-built). A theme therefore mints its private vocabulary as a tracked
-child node:
-
-```rust
-// src/theme/mod.rs (sketch) — ThemeCtx helper
-/// Create `/theme/<name>` carrying the theme's own token properties
-/// (built pre-Arc with builder add_property + set_defaults_*), null
-/// pimpl, linked under /theme and tracked for unload.
-pub fn create_token_child(&self, props: Vec<Property>) -> Result<SceneNodePtr>;
-```
-
-Rules that fall out:
-
-- `/theme` root = shared tokens, always present. Schema default-wiring
-  (D6 helpers) may reference **only** these — a default must never
-  dangle when its theme is inactive.
-- `/theme/<name>` child = theme-private tokens (e.g. scifi's
-  `glow_color`), present only while that theme is applied. They appear
-  in theme-installed **vals** expressions and wired overrides
-  (journaled, D5/D8) — never in schema defaults.
-- Unload ordering matters: unset widget vals → remove journaled dep
-  edges → unlink the token child. A dead dep edge makes
-  `dep.prop.upgrade()` fail and eval error on every pass, so edges are
-  removed before the node that holds their targets dies. (Defense in
-  depth: eval may skip dead edges rather than error — implementer's
-  choice, the ordering rule is the contract.)
-- Token children are data-only nodes: null pimpl, nothing persisted
-  (only the `theme` enum is), lookup-friendly for debugging
-  (`/theme/scifi`).
-
-### D7: Generalize evaluation from rect to bounded f32 arrays — in the draw path
-
-Extract the `eval_with` pattern into a shared free function; add wrappers:
-
-```rust
-// src/prop/wrap.rs (sketch)
-pub fn eval_f32_multi(
-    prop: &PropertyPtr,
-    atom: &mut PropertyAtomicGuard,
-    role: Role,
-    range: &[usize],
-    extras: Vec<(String, f32)>,
-) -> Result<()> {
-    let mut globals = vec![];
-    for dep in prop.get_depends() {
-        let Some(dep_prop) = dep.prop.upgrade() else {
-            return Err(Error::PropertyNotFound)
-        };
-        globals.push((dep.local_name, SExprVal::Float32(dep_prop.get_f32(dep.i)?)));
-    }
-    globals.extend(extras);
-    let mut changes = vec![];
-    for &i in range {
-        if !prop.is_expr(i)? { continue }
-        let expr = prop.get_expr(i)?;
-        let mut machine = SExprMachine { globals: globals.clone(), stmts: &expr };
-        changes.push((i, machine.call()?.as_f32()?));
-    }
-    prop.set_cache_f32_multi(atom, role, changes).unwrap();
-    Ok(())
-}
-
-impl PropertyColor {
-    /// Globals only (no w/h extras). Called at the top of draw().
-    pub fn eval(&self, atom: &mut PropertyAtomicGuard) -> Result<()> {
-        eval_f32_multi(self.prop(), atom, self.role, &[0, 1, 2, 3], vec![])
-    }
-}
-```
-
-`PropertyRect::eval_with` becomes a thin wrapper (extras = parent w/h).
-`PropertyFloat32::eval` serves `font_size`/spacing (index 0).
-
-**Call sites are in the draw path, not in when_change handlers** — this
-is the same pattern rects already use, and it is what makes evaluation
-timing irrelevant: the pass triggered by a switch recomputes every expr
-index from current token values before reading them. Handlers stay
-invalidation-only (`draw_cache.clear(); redraw.trigger()` — the existing
-`text.rs:257-260` shape). The cache writes use the widget wraps'
-`Role::Internal`, so `when_change_external` skips them as eval echoes,
-exactly like rect evals today.
-
-### D8: Theme engine with tracked nodes, tasks, and a journal
-
-`src/theme/`: trait, compile-time registry, and a per-application ctx:
-
-```rust
-pub trait Theme: Send + Sync {
-    fn name(&self) -> &'static str;
-    fn apply<'a>(&'a self, ctx: &'a ThemeCtx) -> BoxFuture<'a, Result<()>>;
-}
-
-/// Unload bookkeeping. Nodes are the storage: theme-created properties
-/// live on tracked nodes, theme tasks are pushed onto tracked nodes
-/// (SceneNode::push_task) and cancel with clear_tasks(); the ctx only
-/// records what cannot be reconstructed.
-///
-/// The touched-set is required, not an optimization: `vals` carries no
-/// authorship, so a theme override is indistinguishable from schema
-/// setup or runtime state (scroll, typed text, is_visible) — unloading
-/// by deep-walking and resetting every property would destroy runtime
-/// state. Unload therefore resets exactly the (prop, i) pairs the
-/// theme recorded, which also means the ctx is the ONLY sanctioned way
-/// for a theme to modify properties it does not own.
-pub enum JournalEntry {
-    /// Bounded `vals` override — unload unsets it (falls to default).
-    /// No prior value needed: defaults ARE the baseline.
-    Touched { prop: PropertyPtr, i: usize },
-    /// Unbounded/direct-list override (e.g. nick_colors) — unbounded
-    /// props have no defaults tier, so the prior entries are recorded.
-    List { prop: PropertyPtr, prior: Vec<PropertyValue> },
-    /// Theme-added dependency edge (D5), removed on unload.
-    Depend { prop: PropertyPtr, dep_prop: PropertyPtr, i: usize, local_name: String },
-}
-
-/// What a theme (and the engine) may do; everything done through the
-/// ctx is undone by unload.
-pub struct ThemeCtx {
-    app: AppPtr,
-    /// Roots the theme linked into pre-existing (schema) trees —
-    /// including its `/theme/<name>` token child. Descendants ride
-    /// along: unlink drops the subtree.
-    nodes: SyncMutex<Vec<SceneNodePtr>>,
-    journal: SyncMutex<Vec<JournalEntry>>,
-}
-
-impl ThemeCtx {
-    /// Link a theme-owned node under an existing parent; unlinked on
-    /// unload (clear_tasks() cancels its tasks, unlink() drops the
-    /// subtree).
-    pub fn link_tracked(&self, parent: &SceneNodePtr, child: SceneNodePtr);
-    /// Build `/theme/<name>` carrying theme-defined token properties
-    /// (D6); linked, tracked, data-only — also the home for theme
-    /// tasks (created lazily for themes with no other nodes).
-    pub fn create_token_child(&self, name: &str, props: Vec<Property>) -> Result<SceneNodePtr>;
-    /// Push a task onto a theme-owned node so teardown cancels it.
-    pub fn push_task(&self, node: &SceneNodePtr, task: smol::Task<()>);
-    /// Set a bounded property value (stamped Role::Theme) — journal
-    /// records only (prop, i); dedups repeat entries (e.g. per-step
-    /// animation writes).
-    pub fn set_touched(&self, atom: &mut PropertyAtomicGuard, prop: &PropertyPtr,
-                       i: usize, val: PropertyValue) -> Result<()>;
-    /// Overwrite an unbounded list (stamped Role::Theme) — journal
-    /// records the prior entries.
-    pub fn set_list(&self, atom: &mut PropertyAtomicGuard, prop: &PropertyPtr,
-                    vals: Vec<PropertyValue>) -> Result<()>;
-    /// Add a dependency edge, recording it for removal (D5).
-    pub fn depend_journaled(&self, prop: &PropertyPtr, dep: &PropertyPtr,
-                            i: usize, local_name: String);
-}
-
-pub fn registry_lookup(name: &str) -> Option<&'static dyn Theme>;
-```
-
-`minimal` is the registry's identity element: it has no `apply`
-implementation (or an empty one) — the minimal look *is* the unloaded
-state (token defaults + schema defaults), so switching to minimal is
-just unload. Theme **behavior** is property watchers — scifi's fade:
-
-```rust
-// src/theme/scifi.rs (sketch)
-async fn apply(ctx: &ThemeCtx) -> Result<()> {
-    let tokens = ctx.create_token_child("scifi", private_token_props())?;
-    set_tokens(ctx, &[("accent_color", [0., 0.94, 1., 1.]), /* … */]).await?;
-    king_video_node(ctx).await?;                 // tracked structural node
-    if ctx.app.is_first_time.load(Ordering::Relaxed) {
-        scramble_splash(ctx).await?;             // tracked node + hide task (D12)
-    }
-
-    // Watch the overlay toggle (existing property, existing pubsub) and
-    // animate alpha. Replaces the fade inside the reconnect click
-    // handler in src/app/schema/mod.rs. The watcher is a task pushed
-    // onto the theme's token child — cancelled with clear_tasks() when
-    // that node is unlinked (tasks live on nodes, D8).
-    let overlay = ctx.app.sg_root.lookup_node("/window/content/chat/netstatus_overlay").unwrap();
-    let is_visible = overlay.get_property("is_visible").unwrap();
-    let alpha = overlay.get_property("alpha").unwrap();
-    let sub = is_visible.subscribe_modify();
-    let ex = ctx.app.ex.clone();
-    let fade_task = ex.spawn(async move {
-        loop {
-            let Ok((_, _, guard)) = sub.receive().await else { break };
-            if !is_visible.get_bool(0).unwrap() { continue }
-            for step in 1..=50 {
-                msleep(20).await;
-                let atom = &mut guard.spawn();
-                // Role::Theme via ctx; touched-set entry dedups across steps
-                ctx.set_touched(atom, &alpha, 0, PropertyValue::Float32(step as f32 / 50.))?;
-            }
-        }
-    });
-    ctx.push_task(&tokens, fade_task);
-    Ok(())
-}
-```
-
-The engine (not the trait) performs unload from the ctx state, so themes
-cannot leak by forgetting cleanup. Unload order is part of the contract:
-`clear_tasks()` on tracked nodes **first** (cancel the actors — an
-in-flight watcher writing `alpha` after its unset would leave a stale
-value nothing resets) → unset `Touched` entries → restore `List` entries
-→ remove `Depend` edges → `clear_values` on shared tokens → `unlink()`
-per tracked node (drops subtrees and their properties; token children
-included). Edges come off before the nodes holding their targets die, so
-no eval ever sees a dangling dep (D6).
-
-**`Role::Theme`.** `vals` carries no authorship, so attribution lives on
-the event stream: a new `Role::Theme` variant, stamped by every ctx
-setter (`set_touched`, `set_list`, shared/private token sets). Consumer
-audit: `when_change_impl` filters by equality (`ui/mod.rs:279-281`,
-skipping only `Internal`/`Ignored`), so `Theme` events reach widgets
-exactly like `App` ones — no filter change, but verified during
-implementation; `net.rs`/`setting.rs` produce roles rather than match
-them, and any external Role mapping needs a `Theme` arm. Role is
-bin/app-local (not serialized), so no wire impact. Bonus this buys: a
-dev-mode leak audit — a `Role::Theme` event arriving on a (prop, i) not
-in the touched-set means something bypassed the ctx; warn-log it. The
-ctx setters take no role parameter; `Role::Theme` is not caller-choice.
-
-### D9: Atomic switch flow — real guard mechanics
-
-One batch for the whole switch; structural changes get an explicit
-trigger because unlinking notifies no properties (same as `menu/mod.rs`):
-
-```rust
-// src/theme/mod.rs (sketch)
-pub async fn switch(app: &App, current: &mut InstalledTheme, next_name: &str) -> Result<()> {
-    let ctx = ThemeCtx::new(app);
-    {
-        let atom = &mut app.redraw_trigger.make_guard(gfxtag!("theme switch"));
-
-        // Unload: clear_tasks (cancel actors) → unset Touched →
-        // restore Lists → remove Depend edges → unset shared token
-        // vals → unlink tracked nodes
-        unload_current(current, &ctx, atom);
-
-        // Load: next theme's tokens/nodes/watchers
-        let next = registry_lookup(next_name)
-            .or_else(|| registry_lookup(DEFAULT_THEME))
-            .ok_or(Error::ThemeNotFound)?;
-        if let Some(apply) = next.apply_fn() { apply(&ctx).await?; }
-        *current = InstalledTheme::from(next, ctx);
-    } // guard Drop: notify all queued actions under one BatchGuard id
-
-    // Unlinks changed the tree but no properties; request a pass
-    // explicitly (coalesced by the bounded(1) channel — free if the
-    // batch already triggered one).
-    app.redraw_trigger.trigger();
-    Ok(())
-}
-```
-
-What "atomic" concretely means here, per `guard.rs`/`ui/mod.rs`:
-
-1. All unload+apply property actions are queued in one guard and
-   notified together at Drop under a single `BatchGuardId` — no listener
-   ever sees a half-switched batch.
-2. Widget handlers that react clear draw caches and enqueue triggers;
-   the bounded(1) redraw channel coalesces every trigger into one pass.
-3. That pass re-evaluates all expr indices draw-side (D7) from the
-   already-settled token values, so the frame is internally consistent
-   by construction — mixed state cannot be drawn, not merely unlikely.
-
-Startup application and live switching share this function; the
-`/setting/theme` watcher is an **engine-owned** task (not theme-tracked,
-never unloaded with a theme).
-
-### D10: Setting integration and robustness
-
-`theme` enum property in `create_setting` (items `minimal`, `scifi`;
-default `scifi` installed via D2's `set_default_enum` — Enum variant,
-not Str). Persistence is free via the `Setting` pimpl. Read-side guard:
-unknown persisted theme → default (spec scenario). Fix `Setting::new`
-to **skip** persisted keys that no longer exist as node properties
-instead of `unwrap()`-panicking — a pre-existing latent bug this change
-makes reachable (persisted `theme` vs. older binary, or a renamed
-setting later). Settings screen gains a minimal enum control
-(cycle-on-tap row); theme names are shown raw (no i18n) in v1, matching
-existing settings rows.
-
-### D11: Sequencing — PaperLight dies first
-
-All 14 PaperLight-bearing files are reworked again by the split; deleting
-the dead scheme first halves the branch noise and makes the minimal/scifi
-diff reviewable. Mechanical collapse: `match COLOR_SCHEME` arms inline to
-their DarkMode values; `ColorScheme`/`COLOR_SCHEME` and the window-linked
-`bg` vector-art block are deleted.
-
-### D12: The splash is scifi-owned
-
-The first-run scramble splash is theme content: the scramble effect and
-its colors are scifi flavor, not baseline structure. It moves out of
-`schema::make` into scifi's `apply` as a tracked node plus a tracked
-hide task, gated on `is_first_time` (readable from the app context at
-apply time, before it is consumed in `App::start`). Consequence: the
-minimal theme has no splash — first run under minimal goes straight to
-the baseline UI, which the completeness requirement already covers.
-
-### D13: Property permissions — Role bitflags + read/write masks
-
-`Role` becomes a bitflag set and properties carry a permission pair;
-setters/getters check the acting role against the mask and fail with a
-new `Error::PropertyPermissionDenied`:
-
-```rust
-// src/prop/mod.rs (sketch) — hand-rolled u8 bit ops, no new dependency
-pub struct Role: u8 {
-    const User     = 1 << 0;
-    const App      = 1 << 1;
-    const Internal = 1 << 2;
-    const Ignored  = 1 << 3;   // marker ("don't notify"), rarely in masks
-    const Theme    = 1 << 4;
-}
-
-pub struct PropertyPermission {
-    /// Roles allowed to read.
-    pub read: Role,
-    /// Roles allowed to write (set/unset/push/insert/remove/clear/expr).
-    pub write: Role,
-}
-
-impl Property {
-    pub fn new<S: Into<String>>(name: S, typ: PropertyType, subtype: PropertySubType,
-                                permission: PropertyPermission) -> Self;
-    pub fn can_read(&self, role: Role) -> bool;
-    pub fn can_write(&self, role: Role) -> bool;
-}
-```
-
-Hand-rolled bit ops rather than the `bitflags` crate: `bitflags` is not
-a dependency of `bin/app` today, and adding one is a supply-chain
-decision requiring human review (repo rules) — for a two-flag type the
-newtype is trivial.
-
-**Write enforcement is centralized and immediate**: every mutating API
-already carries a role (`set_*`, `set_expr`, `unset`, `clear_values`,
-`push_*`, `insert_*`, `remove_*`) — a single `can_write` check before
-any mutation or journal entry; denial returns
-`PropertyPermissionDenied` with the property untouched. This makes two
-existing conventions *enforced invariants*: D4 (themes cannot override
-widget-written properties — the write mask simply lacks `Theme`) and
-the ctx-only rule (a theme writing outside `set_touched` still stamps
-`Role::Theme` and is denied at the property layer if the mask forbids
-it).
-
-**Read enforcement is staged by where an actor is attributable** — raw
-`get_*` calls carry no role (145 sites) and stay trusted in-crate for
-now:
-
-1. Wrap layer: every widget reads through `Property{Float32,Color,…}`
-   wraps, which already hold a role from `wrap()` — `get()` checks
-   `can_read`, and `wrap()` itself validates the role upfront (failing
-   construction instead of at first read).
-2. Expr evaluation: `eval_f32_multi` reads dependencies on behalf of a
-   wrap role — dep reads go through the check.
-3. External boundary: `net.rs` RPC property reads check against the
-   remote actor's role.
-
-Full role parameters on raw getters is future hardening, deliberately
-not in this change (each of the 145 sites would need a reasoned role,
-not a mechanical one).
-
-**Exemptions** (by design): `set_default_*` (construction metadata, D2),
-`set_cache_*` (derived eval artifacts, written `Internal`), and
-`add_depend` (wiring metadata). Modifying a default still cannot be
-gated by write masks — defaults are installed before the tree is live.
-
-**Factory masks make the style/structure/function split concrete**
-(assigned during the 5.1 scrub; transitional default
-`PropertyPermission { read: all, write: all }` keeps current behavior
-until then):
-
-```
-prop class                     read                write
-─────────────────────────────  ──────────────────  ────────────────────
-themeable style (colors,       all internal roles  App | Theme
-font_size, padding, spacing)
-widget-computed/runtime        all internal roles  Internal (| App for
-(scroll, is_focused,                               schema bootstrap)
-height, select_text, alpha
-on multiline rect[3])
-behavior toggles               all internal roles  App (never Theme —
-(is_active, is_visible)                            themes don't change
-                                                   what the UI does)
-content/data (text, items,     all internal roles  App (User where
-nick_colors)                                       user input persists)
-settings (/setting/*)          all                 User | App
-tokens (/theme/*)              all                 Theme (set at
-                                                   apply, unset at
-                                                   unload — nobody
-                                                   else writes them)
-```
-
-`Role::Theme` stamping (D8) and this table are two views of the same
-contract: the mask says who may, the role on the event says who did.
-
-## Structural changes
-
-```
-bin/app/src/
-├── prop/mod.rs        + set_default_{bool,u32,f32,str,enum,expr,value}
-│                        (post-Arc, D2); effective is_expr/get_expr/get_value (D3);
-│                        builder set_defaults_expr; Role bitflags +
-│                        PropertyPermission on Property::new + can_read/
-│                        can_write + write-path enforcement (D13); unit tests
-├── error.rs           + Error::PropertyPermissionDenied (D13)
-├── prop/wrap.rs       + eval_f32_multi free fn; PropertyColor::eval;
-│                        PropertyFloat32::eval; PropertyRect::eval_with delegates (D7)
-├── app/node.rs        factories: allow_exprs() on themeable props + Tier-0 scrub (D6)
-├── theme/mod.rs   NEW Theme trait, ThemeCtx (nodes-as-storage: touched-set,
-│                        list/dep journal, token children, tasks on nodes),
-│                        registry, switch(), apply_startup(),
-│                        create_token_node(), create_token_child(),
-│                        wire_color/wire_f32 (D6-D9)
-├── theme/scifi.rs NEW scifi impl: tokens, king video, splash, fade watcher (D8, D12)
-├── app/mod.rs         App::setup: link /theme before schema::make; apply_startup after
-├── setting.rs         theme enum prop; skip unknown persisted keys (D10)
-├── app/schema/**      PaperLight removal; Tier-1 defaults + wiring helpers (D1, D11)
-└── app/schema/settings.rs  enum rendering (cycle-on-tap) (D10)
-```
-
-`minimal` has no file: it is the unloaded state.
-
-## Flows
-
-### Setup and startup application
-
-```
-main.rs
- └─ App::setup(kv_db, app_db)
-     ├─ link /setting      Setting pimpl loads persisted props synchronously
-     │                     (incl. `theme`; unknown keys skipped — D10)
-     ├─ link /window
-     ├─ link /theme        NEW: token props constructed with minimal
-     │                     palette as defaults (D2, no events)
-     ├─ schema::make(...)  structure + Tier-1 defaults + wire_color/wire_f32
-     │                     (helpers read /theme; all depends wired here — D5)
-     └─ theme::apply_startup()
-         ├─ read /setting/theme → "scifi" (unknown → default — D10)
-         ├─ switch(app, ∅ → scifi)                       (D9)
-         │   └─ scifi tokens set → notifications queue (unbounded — safe,
-         │      widget listener tasks don't exist yet; they drain later)
-         └─ spawn engine-owned /setting/theme watcher (live switches)
-
- App::start(event_pub, epoch)
- ├─ window.init(); redraw_trigger.trigger()     (first pass queued)
- └─ start_procs → Window::start                 draw loop + widget OnModify
-                                                tasks start; queued token
-                                                notifications drain into
-                                                handlers (cache clears)
-      └─ first draw pass: every widget re-evals its expr-bound props
-         (rect + colors + fonts — D7) from settled scifi token values
-         → first frame is fully scifi (spec: applied before first frame)
-```
-
-### Live switch (scifi → minimal)
-
-```
-user taps theme row in settings screen
- └─ setting prop `theme` set → Setting pimpl persists; engine watcher fires
-     └─ switch(app, scifi → minimal)
-         ┌──────────────────────────────────────────────────────────┐
-         │ atom = redraw.make_guard("theme switch")                  │
-         │                                                            │
-         │ UNLOAD scifi (journal replay in reverse + teardown):        │
-         │ UNLOAD scifi (cancel actors, then unwind):                  │
-         │   clear_tasks() on tracked nodes FIRST (in-flight fade      │
-         │     stops before it can write stale vals)                   │
-         │   unset Touched entries → vals fall to schema defaults      │
-         │   restore List entries (nick_colors priors)                 │
-         │   remove Depend edges (incl. private-token refs)           │
-         │   shared token.clear_values() × ~30  (Unset, queued)       │
-         │   unlink() tracked nodes — /theme/scifi dies with its      │
-         │     props and tasks, king video, splash                    │
-         │                                                            │
-         │ LOAD minimal: registry identity element — nothing to do   │
-         └──────────────────────────────────────────────────────────┘
-         drop(atom) ─▶ one notification wave (single BatchGuardId)
-                         │
-                         ├─ widget handlers: draw_cache.clear() + trigger()
-                         │  (bounded(1) channel coalesces all triggers)
-                         └─ last BatchGuard ref drops ─▶ one redraw token
-                                                        (plus the explicit
-                                                        trigger for unlinks)
-                         │
-                         ▼
-         draw pass: draw-side eval reads token *defaults* (= minimal
-         palette) → one consistent minimal frame; scifi nodes absent
-         from the tree; no residue (spec: atomic switching, teardown)
-```
-
-### Redraw guarantee (why no mixed frame is possible)
-
-- Property mutations are only observed through the draw pass; a pass
-  only starts by draining a token from the bounded(1) channel, and tokens
-  are enqueued after state is settled (`make_guard` defers to end-of-
-  batch; handlers mutate then trigger).
-- Passes re-evaluate every expr index from current dependency values
-  before reading them (D7) — a pass cannot draw a stale cache even if it
-  races a handler.
-- Structural-only changes (unlink) notify nothing, hence the explicit
-  `trigger()` in `switch` — mirroring the `menu/mod.rs` precedent.
-
-## Risks / Trade-offs
-
-- [Theme overrides on widget-written properties get clobbered] → D4
-  ownership rule, audited per widget during the schema split; enforcement
-  metadata deferred.
-- [Live dependency rewiring touches the widget hot path
-  (`when_change_impl`)] → D5 keeps the existing poll loop; resync is
-  rare (theme switches), adds one subscription per watched prop, and
-  the post-resync handler run is the existing coalesced
-  invalidate+trigger.
-- [Draw-side eval adds per-pass cost] → same order as today's rect
-  evals; exprs are tiny (var loads + arithmetic); only wired (themeable)
-  props carry exprs.
-- [`Setting::new` panics on unknown persisted keys] → fixed as part of
-  D10 (skip-and-log), before the new `theme` key ships.
-- [Theme node z-order collisions with schema layers] → theme-owned
-  background/decoration nodes take a reserved low band (`z_index` 0 under
-  `/window/content`); convention documented with the engine.
-- [Volume: 23 schema files to convert] → helpers land first, then
-  per-area conversion (menu, chat, wallet, settings, root); PaperLight
-  removal (D11) precedes; visual check per area under both themes.
-- [VectorArt shape colors not tokenizable] → scifi rebuilds its themed
-  shapes structurally; neutral shapes keep baked colors (non-goal).
-- [First-frame ordering: theme applied before widget tasks exist] → safe
-  by construction: pubsub queues are unbounded (events buffer), and the
-  first pass evaluates draw-side from settled token values (Flows).
-
-## Migration Plan
-
-Single change, no flag: default `scifi` means the shipped look is
-unchanged on upgrade — the new `/setting/theme` key is the only persisted
-addition, and the D10 skip-guard makes it harmless to older binaries.
-Rollback is revert; any persisted `theme` value is ignored (skipped)
-by pre-change code once D10's guard is in.
-
-Implementation order is the tasks order: PaperLight removal → property
-APIs → evaluation → tokens/wiring → schema split → engine → scifi →
-setting/UI. Each step keeps `make compile-dev` green; behavior
-verification per area under both themes.
-
-## Open Questions
-
-- Exact shared token vocabulary (`accent_color` vs split accent/dim/
-  accent2, how many `edit.*`/`menu.*`/`chatview.*` tokens): grown by
-  trial and error during the split; does not affect the mechanism. The
-  pressure is lower than it first looks — only *schema-referenced*
-  tokens must exist up front; themes mint anything else privately (D6).

+ 0 - 103
openspec/changes/app-theme/proposal.md

@@ -1,103 +0,0 @@
-## Why
-
-The app's entire look — the scifi cyan palette, the king video background,
-the scramble splash, overlay fades — is hard-coded inline across ~14 schema
-files (233 `set_property_*` styling calls, 125 `text_color` touches) and has
-even leaked into the node factories in `node.rs` (`action_fg_color`,
-`url_copy_*`, `scramble_color`, menu role colors). The only variation
-mechanism is a compile-time `COLOR_SCHEME` switch whose PaperLight arm is
-dead code. There is no way to change the look at runtime, no defined
-boundary between "style" and "structure", and no reset point themes could
-start from. This change makes the UI themable at runtime by layering themes
-on the property system's existing defaults/vals fallback, turning the
-current look into one selectable theme (`scifi`) over a neutral `minimal`
-baseline.
-
-## What Changes
-
-- **Property system (`bin/app/src/prop/`)**: defaults become installable on
-  live (already-linked) properties via a post-creation `set_default_*` API
-  for every type; defaults may hold SExpr; "effective expr" semantics —
-  `is_expr`/`get_expr` fall through `vals` → `defaults`, and `get_value`
-  never returns an unresolved SExpr (resolves via cache, else type
-  default). Factories opt in `allow_exprs()` for themeable props (it is
-  builder-only today, so live properties can never gain expr support).
-  `Role` becomes a bitflag set and properties gain
-  `PropertyPermission` (read mask, write mask) enforced on writes
-  (every mutating API already carries a role) and on attributed reads
-  (wrap layer, expr evaluation, RPC boundary), failing with
-  `PropertyPermissionDenied` — which turns "themes don't override
-  widget-owned properties" from convention into a hard invariant.
-- **Expr evaluation beyond rects**: f32-array properties (4-component
-  colors, font sizes, spacing) gain per-index expr evaluation against
-  dependency globals, re-evaluated in widgets' draw paths alongside rect
-  evaluation (today only `PropertyRect` has this); `when_change` handlers
-  stay invalidation-only.
-- **`/theme` token node**: a scene-root node holding the shared
-  themeable vocabulary as token properties whose defaults are the
-  `minimal` palette. Schema creation code wires styled properties to
-  tokens via helper functions ("classes") that install default-exprs
-  plus `add_depend` edges — one wiring, every wired widget tracks the
-  token forever, including nodes created at runtime. Themes can also
-  mint their own private tokens as tracked child nodes
-  (`/theme/<name>`), usable in overrides, gone on unload — themes are
-  not boxed in by the shared list.
-- **Minimal baseline**: the schema installs Tier-1 defaults (complete,
-  neutral, usable) instead of inline styling; factories are scrubbed of
-  theme leaks back to neutral Tier-0 type defaults.
-- **Theme engine (`bin/app/src/theme/`)**: `Theme` trait + registry;
-  `apply`/unload with nodes as the storage unit — theme-created
-  properties live on tracked nodes (private tokens under
-  `/theme/<name>`), theme tasks are pushed onto those nodes and cancel
-  with them; every theme modification is stamped with a new
-  `Role::Theme` (attribution on the event stream — `vals` stores no
-  authorship); unload cancels theme tasks first, then resets only the
-  recorded touched properties (a deep-walk reset is neither possible
-  nor safe), restores unbounded-list priors (e.g. `nick_colors`, which
-  have no defaults tier), removes theme-added dependency edges, and
-  unlinks tracked nodes — all as one atomic property batch. Theme
-  behavior (e.g. the netstatus overlay fade) is implemented as property
-  watchers, not schema hooks.
-- **scifi extracted**: the first real theme — cyan token palette, king
-  video background node, fade watchers.
-- **`/setting/theme` enum** (`minimal` | `scifi`, default `scifi`),
-  persisted through the existing `Setting` pimpl, applied after schema
-  load, live-switched on change; the settings screen gains enum
-  rendering (enums currently display as "unknown").
-- **PaperLight removed**: `ColorScheme` enum, the `COLOR_SCHEME` const, and
-  all two-arm style branches (collapsing to the DarkMode values) are
-  deleted across 14 files, including the dead window-linked `bg`
-  vector-art block.
-
-## Capabilities
-
-### New Capabilities
-
-- `prop-defaults`: post-creation default installation on live properties
-  (all types incl. SExpr), effective-expr resolution order for reads, and
-  f32-array expr evaluation with dependency-triggered re-evaluation.
-- `app-theme`: the runtime theme system — token node and class wiring,
-  minimal baseline, theme apply/unload lifecycle (tracked nodes, tasks,
-  journal), atomic switching, the `theme` setting, and scifi as first
-  theme.
-
-### Modified Capabilities
-
-(none — no existing specs in this repo; the property-system extensions are
-new behavior captured in `prop-defaults`.)
-
-## Impact
-
-- `bin/app/src/prop/mod.rs`, `src/prop/wrap.rs` (default APIs, effective
-  exprs, f32-multi evaluation) + unit tests in the existing test module.
-- `bin/app/src/ui/` — color/font expr re-evaluation call sites in
-  widgets' `when_change` update paths.
-- `bin/app/src/app/node.rs` (factory default scrub), `src/setting.rs`
-  (theme enum), `src/app/schema/settings.rs` (enum rendering).
-- `bin/app/src/app/schema/**` (all files: PaperLight removal, minimal
-  defaults, token wiring) and `src/app/mod.rs` (apply theme after
-  `schema::make`).
-- New `bin/app/src/theme/` module.
-- No changes outside `bin/app`; no new dependencies. Verified via
-  `bin/app` Makefile (`make compile-dev`, `make compile-apk`) and the
-  property unit tests.

+ 0 - 163
openspec/changes/app-theme/specs/app-theme/spec.md

@@ -1,163 +0,0 @@
-## Purpose
-
-Runtime theme system for the app UI: a neutral minimal baseline installed
-as property defaults, a token vocabulary wired into styled properties, and
-a switchable theme lifecycle (apply, unload, live switch) layered on top —
-so the entire visual style, including theme-owned decorations and
-animations, can change at runtime without restart.
-
-## ADDED Requirements
-
-### Requirement: Theme selection setting
-
-The app SHALL expose a `theme` setting under `/setting` as an enum of the
-available theme names, persisted across restarts. The default SHALL be
-`scifi`. A persisted value that is not among the available themes SHALL
-be treated as the default.
-
-#### Scenario: First run defaults to scifi
-
-- **WHEN** the app starts with no persisted theme setting
-- **THEN** the active theme is scifi and the UI matches the current shipped look
-
-#### Scenario: Selection persists across restart
-
-- **WHEN** the user switches the theme and later restarts the app
-- **THEN** the persisted theme is active on startup
-
-#### Scenario: Unknown persisted value falls back
-
-- **WHEN** the persisted theme is not an available theme (e.g. removed in a
-  later version)
-- **THEN** the app starts with the default theme instead of failing
-
-### Requirement: Theme application at startup
-
-After the schema is constructed, the persisted theme SHALL be applied
-before the first drawn frame. With `minimal` selected, the UI SHALL render
-the minimal baseline with no theme decorations.
-
-#### Scenario: App starts themed
-
-- **WHEN** the app starts with any valid persisted theme
-- **THEN** the first drawn frame already reflects that theme
-
-#### Scenario: Minimal at startup
-
-- **WHEN** the app starts with `minimal` persisted
-- **THEN** the UI renders the baseline with no theme-owned nodes (no video
-  background, no splash, no themed fades)
-
-### Requirement: Layered theming over a minimal baseline
-
-The minimal baseline SHALL be installed as property defaults during schema
-construction. A theme SHALL override properties by setting values (plain
-or expression) in the value slot, and MAY override any property type,
-including rect geometry. Unloading a theme SHALL restore the baseline for
-every property it touched, without recreating nodes or restarting.
-
-#### Scenario: Rect override restores on switch
-
-- **WHEN** a theme overrides a widget's rect and the theme is later unloaded
-- **THEN** the widget's rect returns to the baseline geometry
-
-#### Scenario: No styling residue after unload
-
-- **WHEN** a theme that set colors, fonts, and spacing is unloaded
-- **THEN** every property it touched reads its baseline value
-
-### Requirement: Atomic theme switching
-
-Switching themes SHALL unload the current theme and apply the new one as a
-single atomic property modification batch, followed by a redraw. The UI
-SHALL reflect only the new theme afterward, with no restart and no
-permanently mixed state.
-
-#### Scenario: Live switch end to end
-
-- **WHEN** the user switches from scifi to minimal while a screen is open
-- **THEN** in the next redraw the whole UI shows the minimal look (accents,
-  background, and decorations all changed together)
-
-### Requirement: Theme-created node and task teardown
-
-Nodes created by a theme SHALL be tracked, and on unload SHALL be removed
-from the scene tree with their tasks cancelled. Tasks spawned by a theme
-(including animations reacting to property changes) SHALL stop when the
-theme unloads.
-
-#### Scenario: Theme background node disappears
-
-- **WHEN** switching away from a theme that inserted a background node
-- **THEN** the node is gone from the scene tree and nothing it drew remains
-
-#### Scenario: In-flight animation stops on switch
-
-- **WHEN** a theme-driven fade animation is mid-flight and the theme is
-  switched away
-- **THEN** the animation stops making further changes
-
-### Requirement: Token propagation to late-created nodes
-
-Changing a theme token SHALL update every property wired to that token.
-Nodes created after a theme is applied (e.g. a chat screen for a channel
-joined later) SHALL render with the active theme's styling without any
-re-theming pass.
-
-#### Scenario: Token change updates all wired properties
-
-- **WHEN** a token value changes while the app is running
-- **THEN** all properties wired to that token reflect the new value after the
-  next evaluation
-
-#### Scenario: Late-created node is themed
-
-- **WHEN** a channel is joined and its chat screen constructed while a
-  non-minimal theme is active
-- **THEN** the new screen renders with the active theme's styling
-
-### Requirement: Minimal baseline completeness
-
-The minimal baseline SHALL be a complete, usable look on its own: all
-user-visible surfaces (chat, channel/contact menus, wallet flows, send
-and receive screens, settings, netstatus overlay) SHALL render with
-readable text and visible controls when only the baseline is active.
-
-#### Scenario: Full walkthrough under minimal
-
-- **WHEN** the app runs with the minimal theme and the user visits each
-  surface
-- **THEN** text is legible against its background and interactive controls
-  are visible and usable on every surface
-
-### Requirement: Theme-defined tokens
-
-A theme SHALL be able to define additional token properties beyond the
-shared vocabulary, existing only while that theme is applied. Theme
-overrides and expressions SHALL be able to reference these tokens. On
-unload, theme-defined tokens SHALL cease to exist along with the
-dependencies referencing them, leaving no residue.
-
-#### Scenario: Private token drives an override
-
-- **WHEN** a theme defines a private token, wires a widget property to
-  it, and the token's value is set
-- **THEN** the widget renders per the private token's value while the
-  theme is active
-
-#### Scenario: Private tokens vanish on switch
-
-- **WHEN** the theme that defined private tokens is unloaded
-- **THEN** those tokens and any dependency edges referencing them are
-  gone, and properties that referenced them read their baseline values
-
-### Requirement: Restoration of non-expression themeable properties
-
-For themeable properties that cannot hold expressions (e.g. unbounded
-value lists such as chat nick colors), a theme SHALL record the prior
-value before setting it and restore that value on unload.
-
-#### Scenario: Nick colors restore after switching away
-
-- **WHEN** a theme sets a nick color list and is then unloaded
-- **THEN** the previous nick color list is restored

+ 0 - 131
openspec/changes/app-theme/specs/prop-defaults/spec.md

@@ -1,131 +0,0 @@
-## Purpose
-
-Defines layered value resolution for the app's property system: defaults
-installable on live properties (including expression defaults), the read
-resolution order across set values, expressions, and defaults, and
-per-index float expression evaluation with dependency-triggered
-re-evaluation. This is the substrate that lets any styled or geometric
-property be overridden and later restored to its baseline without
-recreating nodes.
-
-## ADDED Requirements
-
-### Requirement: Post-creation default installation
-
-A default value SHALL be installable on any property of a live, already
-linked node, for every property type (bool, uint32, float32, string,
-enum, node id, shape, and expression). Installing a default on a property
-whose value is unset SHALL change the property's effective value. A value
-set explicitly in the value slot SHALL take precedence over the default,
-and clearing the value SHALL fall back to the installed default.
-
-#### Scenario: Default installed on a live node is effective
-
-- **WHEN** a default is installed on a property of a linked node whose value is unset
-- **THEN** reading the property yields the installed default
-
-#### Scenario: Explicit value overrides an installed default
-
-- **WHEN** a property has both an installed default and an explicitly set value
-- **THEN** reading the property yields the explicitly set value
-
-#### Scenario: Clearing the value falls back to the default
-
-- **WHEN** an explicitly set value is cleared (unset) and a default is installed
-- **THEN** reading the property yields the installed default again
-
-### Requirement: Expression defaults and effective expression source
-
-Defaults SHALL be allowed to hold expressions. When both the value slot
-and the default can supply an expression, the value slot's expression
-SHALL be the active source; when the value slot holds no expression and
-no plain value, the default's expression SHALL be the active source.
-Evaluators SHALL evaluate whichever expression is active.
-
-#### Scenario: Unsetting an override returns control to the default expression
-
-- **WHEN** a property's geometry is governed by a default expression, a theme
-  overrides it with a plain value or its own expression, and the override
-  is then unset
-- **THEN** the property is once again computed from the default expression
-
-#### Scenario: Both slots holding expressions prefers the value slot
-
-- **WHEN** the value slot holds an expression and the default also holds an
-  expression
-- **THEN** evaluation uses the value slot's expression
-
-### Requirement: Reads never expose unresolved expressions
-
-Reading a property's effective value SHALL never yield an expression
-object. Resolution SHALL proceed: set value, then the set expression's
-last computed result, then the default, then the default expression's
-last computed result, then the type's neutral default. Before an
-expression has been evaluated for the first time, concrete-type reads
-SHALL succeed with the next available layer rather than fail.
-
-#### Scenario: Read before first evaluation
-
-- **WHEN** a float property holds an expression that has never been evaluated
-  and no default is installed
-- **THEN** reading it as a float yields the type default instead of an error
-
-#### Scenario: Read after the expression is evaluated
-
-- **WHEN** an expression property has been evaluated and cached
-- **THEN** reading it as a float yields the cached computed result
-
-### Requirement: Role-based property permissions
-
-Roles SHALL be a bitflag set, and every property SHALL carry a
-permission pair (readable roles, writable roles) supplied at creation.
-A write attempt (set, unset, clear, expr, push, insert, remove) by a
-role not in the write mask SHALL fail with a permission-denied error
-and leave the property unmodified. A read by a role not in the read
-mask SHALL fail the same way wherever the acting role is attributable:
-wrapped property handles, expression evaluation of dependencies, and
-external (RPC) property access. Default installation and evaluation
-cache writes SHALL be exempt from write checks. Until factories assign
-masks, a default permission allowing all roles SHALL preserve current
-behavior.
-
-#### Scenario: Denied write does not mutate
-
-- **WHEN** a role lacking the write bit sets a property
-- **THEN** the call returns a permission-denied error and the property's
-  value is unchanged
-
-#### Scenario: Denied wrapped read errors
-
-- **WHEN** a wrapped property handle constructed with a role lacking the
-  read bit reads the property
-- **THEN** the read returns a permission-denied error instead of a value
-
-#### Scenario: Theme cannot write widget-owned properties
-
-- **WHEN** a theme writes a runtime-computed property whose write mask
-  excludes the theme role
-- **THEN** the write is denied and the widget's computed value stands
-
-### Requirement: Float-array expression evaluation with dependency re-evaluation
-
-Bounded float-array properties (for example 4-component colors) SHALL
-support per-index expressions evaluated against globals provided by the
-property's dependencies plus any evaluation extras. Indices holding plain
-values SHALL be left untouched by evaluation. When a dependency of such a
-property changes, the consuming widget SHALL re-evaluate the affected
-indices so that subsequent reads observe the new computed result.
-
-#### Scenario: Dependency change recolors a wired property
-
-- **WHEN** a color property's four indices are expressions referencing a
-  token property, and the token's value changes
-- **THEN** the color property's subsequent reads yield the color computed
-  from the new token value
-
-#### Scenario: Mixed plain and expression indices
-
-- **WHEN** one index of a float-array property holds a plain value and the
-  others hold expressions, and evaluation runs
-- **THEN** only the expression indices are recomputed and the plain index
-  keeps its value

+ 0 - 198
openspec/changes/app-theme/tasks.md

@@ -1,198 +0,0 @@
-## 1. Remove PaperLight (dead code)
-
-- [ ] 1.1 Delete `ColorScheme`, the `COLOR_SCHEME` const, and the dead
-  window-linked `bg` vector-art block in `src/app/schema/mod.rs`; collapse
-  the `if COLOR_SCHEME == ...` background branch to the unconditional
-  video path. Verify `make compile-dev` in `bin/app`.
-- [ ] 1.2 Collapse every `match COLOR_SCHEME` / `if COLOR_SCHEME ==`
-  two-arm branch in `chat.rs`, `menu/{mod,channel,contact}.rs`,
-  `settings.rs`, and `wallet/*.rs` to the DarkMode values inlined, and
-  drop the now-unused `ColorScheme`/`COLOR_SCHEME` imports (14 files
-  total). Verify `make compile-dev` and that the app renders as before.
-
-## 2. Property default APIs and permissions
-
-- [ ] 2.1 Add post-creation default installation on `PropertyPtr` in
-  `src/prop/mod.rs` (`set_default_*` for bool, u32, f32, str, enum, node
-  id, shape, null) mutating `defaults[i]` with no modify event, enforcing
-  the same length/type checks as the builder variants. Add unit tests:
-  install-on-live-node changes effective read; explicit value wins;
-  unset falls back to installed default.
-- [ ] 2.2 Add expression defaults: builder `set_defaults_expr` plus
-  post-creation `set_default_expr`, and make `is_expr`/`get_expr` resolve
-  the effective expression source (`vals` expression, else `defaults`
-  expression). Update `get_value` so it never returns an unresolved
-  expression (resolution order per the `prop-defaults` spec; unevaluated
-  expression reads yield the type default). Unit tests: override+unset
-  returns to default expression; value-slot expression wins over default
-  expression; concrete-type read before first evaluation succeeds.
-  Verify `cargo test prop::` in `bin/app` (crate `darkfi-app`) and
-  `make compile-dev`.
-- [ ] 2.3 Make `Role` a hand-rolled u8 bitflag set (User, App, Internal,
-  Ignored, Theme — no new dependency) and add `PropertyPermission`
-  {read, write} taken by `Property::new` (transitional default = all
-  roles, so all current call sites keep behavior) plus `can_read`/
-  `can_write` and `Error::PropertyPermissionDenied` (design D13).
-  Enforce write masks centrally in every mutating API (role is already
-  a parameter) before any mutation or journaling; enforce read masks in
-  the wrap layer (`wrap()` validates upfront, `get()` checks), in
-  `eval_f32_multi` dependency reads, and at the `net.rs` RPC boundary;
-  exempt `set_default_*` and `set_cache_*`. Unit tests: denied write
-  leaves value unchanged; denied wrapped read errors; theme role denied
-  on a mask without Theme. Verify `cargo test prop::` and `make
-  compile-dev`.
-
-## 3. f32-array expression evaluation beyond rect
-
-- [ ] 3.1 Generalize the `PropertyRect::eval_with` pattern in
-  `src/prop/wrap.rs` into a shared bounded f32-array evaluator and expose
-  `PropertyColor::eval` (globals-only, no extras) plus a single-f32
-  variant for `font_size`/spacing. Unit tests: per-index exprs recompute
-  from dependency globals; plain indices untouched; evaluated results
-  land in the cache and are returned by `get_f32` (note: cache writes
-  bypass `set_f32` range validation by design). Verify `cargo test
-  prop::`.
-- [ ] 3.2 Add draw-path re-evaluation call sites in color/font-consuming
-  widgets (`Text`, `Edit`, `ChatView`, `Menu`, `TextScramble`): at the
-  top of `draw()`, alongside the existing rect evaluation, re-evaluate
-  expr-bound styled props (`PropertyColor::eval`, f32 eval) so every
-  pass computes from current dependency values; keep `when_change`
-  handlers invalidation-only (clear draw cache + trigger), with cache
-  writes as `Role::Internal` so `when_change_external` echo-skips them
-  (same pattern as rect evals today). Verify with a scratch wiring
-  (expr-bound `text_color` reacting to a test property) under `make
-  compile-dev`.
-
-## 4. `/theme` token node and class-wiring helpers
-
-- [ ] 4.1 Create the `/theme` node (linked at scene root beside
-  `/setting`) with an initial token vocabulary: `text_color`,
-  `text_dim_color`, `accent_color`, `bg_color`, `edit.*` colors,
-  `menu.*` colors, `chatview.*` colors, font/spacing f32s — defaults
-  form the neutral minimal palette; write mask = Theme only (D13).
-  Node is created in the app setup path before `schema::make`. Verify
-  node exists at `/theme` with defaults via a startup log or debug
-  lookup; `make compile-dev`.
-- [ ] 4.2 Add wiring helpers (`wire_color`, `wire_f32`) that install
-  default-exprs + `add_depend` edges from a node property onto token
-  components in one call, replacing 4×`set_f32` styling blocks. All
-  wiring happens at creation (design D5); theme-time rewiring goes
-  through `ctx.depend_journaled`. Verify a helper-wired `text_color`
-  tracks a token change at runtime under `make compile-dev`.
-
-## 5. Minimal baseline: schema split and factory scrub
-
-- [ ] 5.1 Scrub theme leaks in `src/app/node.rs` factories back to
-  neutral Tier-0 defaults: `action_fg_color`/`action_bg_color`,
-  `url_copy_*` colors, `scramble_color`, menu `role1/role2` colors,
-  `tokentable` colors; enable `allow_exprs()` on every themeable prop
-  (colors, `font_size`, `padding`, `lineheight`, spacing) — it is
-  builder-only, so without this the wiring helpers' `set_default_expr`
-  calls fail with `PropertySExprNotAllowed`; and assign real
-  `PropertyPermission` masks per the D13 table (themeable style =
-  write App|Theme; widget-computed/runtime = write Internal without
-  Theme, enforcing D4; behavior toggles = write App; content/data =
-  write App) — replacing the transitional allow-all default. Verify
-  `make compile-dev` and note any visual deltas are restored later via
-  tokens/scifi.
-- [ ] 5.2 Convert `src/app/schema/mod.rs` (root, netstatus icons and
-  overlay) to minimal defaults + token wiring: inline styling blocks
-  become `set_default_*` or helper wirings; geometry exprs move to
-  default-exprs; the first-run splash block is removed from the schema
-  (re-implemented by scifi in 7.1 per design D12). Verify both a
-  token-flip recolors the overlay and that with untouched tokens the UI
-  looks minimal-but-correct; `make compile-dev`.
-- [ ] 5.3 Convert `menu/` (mod, channel, contact, edit helpers) to
-  minimal defaults + tokens. Verify menu, channel/contact lists, and
-  edit mode render correctly under the baseline; `make compile-dev`.
-- [ ] 5.4 Convert `chat.rs` per-channel screens (chatview styling, edit
-  composer, selection overlay) to minimal defaults + tokens, including
-  journaled handling for `nick_colors` (unbounded list, no exprs).
-  Verify a freshly joined channel's screen renders themed-if-tokens-set;
-  `make compile-dev`.
-- [ ] 5.5 Convert `wallet/` screens (main, send steps 1-4, receive,
-  tx_status, data, util helpers) and `settings.rs` to minimal defaults +
-  tokens. Verify full wallet flow walkthrough under baseline; `make
-  compile-dev`.
-
-## 6. Theme engine
-
-- [ ] 6.1 Create `src/theme/` with the `Theme` trait (`name()`, async
-  `apply(ctx)`), compile-time registry (`minimal`, `scifi`), and
-  `ThemeCtx` using nodes-as-storage (design D8): tracked node roots
-  (incl. theme-defined token children under `/theme/<name>`, built
-  pre-Arc with builder `add_property`), theme tasks pushed onto
-  theme-owned nodes (`push_task` → cancelled by `clear_tasks()` on
-  unload), and a journal of `Touched` (prop, i — unset on unload),
-  `List` (unbounded-list priors, e.g. nick_colors — unbounded props
-  have no defaults tier), and `Depend` (theme-added edges — removed on
-  unload). All ctx setters stamp `Role::Theme` on the modify events
-  (attribution lives on the event stream since `vals` has none); verify
-  `when_change_impl`'s equality filters pass `Theme` through like `App`
-  (only Internal/Ignored are skipped) and audit other Role consumers
-  (`net.rs`, `setting.rs` produce roles; any external Role mapping
-  needs a `Theme` arm); optional dev-mode leak audit warns on
-  `Role::Theme` events for (prop, i) pairs not in the touched-set. The
-  touched-set is required for correctness (`vals` has no authorship; a
-  deep-walk reset would destroy runtime state) — the ctx is the only
-  sanctioned path for themes to modify properties they don't own (and
-  D13 masks deny off-path writes outright). Unload order:
-  `clear_tasks()` on tracked nodes FIRST (cancel actors — an in-flight
-  watcher writing after its unset would leave stale vals) → unset
-  Touched → restore Lists → remove Depend edges → unset shared tokens →
-  `unlink()` per tracked node. Verify a synthetic theme that touches
-  one token, defines one private token wired to a widget, overrides a
-  bounded prop and an unbounded list, inserts one node, and pushes one
-  watcher task unloads cleanly (second apply leaves no residue, no
-  dangling edges, watcher cancelled) via a temporary schema-test hook
-  or unit test; `make compile-dev`.
-- [ ] 6.2 Implement the atomic switch flow per design D9: single
-  `redraw.make_guard` batch (unload + apply), explicit `trigger()` after
-  the drop for structural-only changes (unlinks notify nothing); wire it
-  to the `/setting/theme` property — `apply_startup()` in `App::setup`
-  after `schema::make`, plus an engine-owned watcher task (not
-  theme-tracked) for live switches. Verify startup applies the persisted
-  theme before the first frame (pubsub queues buffer events until widget
-  tasks start; the first pass evaluates draw-side) and a live setting
-  change switches atomically; `make compile-dev`.
-
-## 7. scifi theme
-
-- [ ] 7.1 Extract the scifi look into `src/theme/scifi.rs`: cyan token
-  values, at least one theme-defined private token (proves the D6
-  private-token path, e.g. the netlogo accent), the king video
-  background as a tracked structural node (reserved low z band under
-  `/window/content`), the first-run scramble splash as a tracked node +
-  hide task gated on `is_first_time` (design D12), and the netstatus
-  overlay fade as a tracked watcher on `is_visible` writing `alpha` via
-  `ctx.set_touched` (remove the in-handler fade from the reconnect
-  click handler in `src/app/schema/mod.rs`). Verify: with `theme=scifi`
-  the app is pixel-comparable to today's look (video bg, splash on
-  first run, cyan accents, fade on overlay open); `make compile-dev`
-  and `make compile-apk`.
-
-## 8. Theme setting and settings UI
-
-- [ ] 8.1 Add the `theme` enum property to `create_setting` in
-  `src/setting.rs` (items `minimal`, `scifi`; default `scifi`); fix
-  `Setting::new` to skip-and-log persisted keys that no longer exist as
-  node properties instead of unwrapping; guard reads so an unknown
-  persisted theme falls back to the default. Verify persistence across
-  restart and the fallback path (hand-edit the persisted value);
-  `make compile-dev`.
-- [ ] 8.2 Add enum rendering to the settings screen (`settings.rs`):
-  cycle-on-tap row for enum values (starts with `theme`; also fixes
-  `net.transport` showing "unknown"). Verify both enums display and
-  modify correctly, and switching `theme` there drives the live switch;
-  `make compile-dev`.
-
-## 9. End-to-end verification
-
-- [ ] 9.1 Full walkthrough under both themes (chat, menus, wallet send
-  flow, receive, settings, netstatus overlay, emoji picker): minimal is
-  complete and usable per the `app-theme` spec; scifi matches the
-  current shipped look; live switch mid-session leaves no mixed state,
-  no orphaned visuals, no running animations from the old theme; theme
-  writes outside permitted masks are denied (spot-check a widget-owned
-  prop via a debug theme write). Verify `make compile-dev` clean,
-  `cargo test` in `bin/app` green, and `make compile-apk` for Android.