Explorar el Código

app/menu/channel: add channel screen with input fields, channel list, copy button and styling

media hace 3 semanas
padre
commit
bd89676179
Se han modificado 1 ficheros con 1318 adiciones y 0 borrados
  1. 1318 0
      bin/app/src/app/schema/menu/channel.rs

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

@@ -0,0 +1,1318 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2026 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use darkfi_serial::deserialize;
+use ui_consts::*;
+
+use super::{
+    edit_buttons, edit_switch::edit_switch, ColorScheme, BTN_TEXT_Y, CHANNEL_ITEM_HEIGHT,
+    COLOR_SCHEME, MENU_BTN_W_L,
+};
+use crate::{
+    app::{
+        node::{
+            create_button, create_layer, create_menu, create_shortcut, create_singleline_edit,
+            create_text, create_vector_art,
+        },
+        App,
+    },
+    expr,
+    gfx::gfxtag,
+    mesh::{COLOR_CYAN, COLOR_INACTIVE, COLOR_MINT, COLOR_MINT_OP, MINT_BTN_GRADIENT,},
+    prop::{PropertyBool, PropertyFloat32, Role},
+    scene::{SceneNodePtr, Slot},
+    shape,
+    ui::{
+        BaseEdit, BaseEditType, Button, Layer, Menu, ShapeVertex, Shortcut, Text, VectorArt,
+        VectorShape,
+    },
+    util::i18n::I18nBabelFish,
+};
+
+#[cfg(any(target_os = "android", feature = "emulate-android"))]
+mod android_ui_consts {
+    pub const LABEL_X: f32 = 40.;
+    pub const LABEL_LINESPACE: f32 = 125.;
+    pub const LABEL_FONTSIZE: f32 = 44.;
+    pub const MENU_SEP_SIZE: f32 = 3.;
+    pub const MENU_HANDLE_PAD: f32 = 110.;
+    pub const MENU_FADE: f32 = 130.;
+    pub const VERBLOCK_SCALE: f32 = 150.;
+    pub const VERBLOCK_X: f32 = 180.;
+    pub const VERBLOCK_Y: f32 = 80.;
+    pub const CHATEDIT_PAD: f32 = 38.;
+    pub const CHATEDIT_HEIGHT: f32 = 115.;
+    pub const TEXTBAR_BASELINE: f32 = FONTSIZE * 0.7;
+    pub const FONTSIZE: f32 = 38.;
+    pub const CHATEDIT_CURSOR_ASCENT: f32 = FONTSIZE * 0.7;
+    pub const CHATEDIT_CURSOR_DESCENT: f32 = FONTSIZE * 0.35;
+    pub const CHATEDIT_SELECT_ASCENT: f32 = 50.;
+    pub const CHATEDIT_SELECT_DESCENT: f32 = 20.;
+    pub const CHATEDIT_HANDLE_DESCENT: f32 = 10.;
+    pub const ACTION_PADDING: f32 = 32.;
+    pub const ACTION_SPACING: f32 = 8.;
+    pub const HEADER_HEIGHT: f32 = 140.;
+    pub const BACKARROW_SCALE: f32 = 30.;
+    pub const BACKARROW_X: f32 = 70.;
+    pub const BACKARROW_Y: f32 = 70.;
+    pub const BACKARROW_BG_W: f32 = 140.;
+    pub const CONTENT_MARGIN: f32 = 30.;
+    pub const COPY_WIDTH: f32 = 200.;
+    pub const COPY_SCALE: f32 = 35.;
+    pub const COPY_BTN_SIZE: f32 = CHATEDIT_HEIGHT;
+    pub const CONTENT_OUTLINE_SIZE: f32 = 0.5;
+}
+
+#[cfg(target_os = "android")]
+mod ui_consts {
+    pub use super::android_ui_consts::*;
+}
+
+#[cfg(feature = "emulate-android")]
+mod ui_consts {
+    pub use super::android_ui_consts::*;
+}
+
+#[cfg(all(
+    any(target_os = "linux", target_os = "macos", target_os = "windows"),
+    not(feature = "emulate-android")
+))]
+mod ui_consts {
+    pub const LABEL_X: f32 = 20.;
+    pub const LABEL_LINESPACE: f32 = 55.;
+    pub const LABEL_FONTSIZE: f32 = 22.;
+    pub const MENU_SEP_SIZE: f32 = 1.;
+    pub const MENU_HANDLE_PAD: f32 = 80.;
+    pub const MENU_FADE: f32 = 130.;
+    pub const CHATEDIT_PAD: f32 = 20.;
+    pub const CHATEDIT_HEIGHT: f32 = 50.;
+    pub const TEXTBAR_BASELINE: f32 = FONTSIZE * 0.7;
+    pub const FONTSIZE: f32 = 18.;
+    pub const CHATEDIT_CURSOR_ASCENT: f32 = FONTSIZE * 0.7;
+    pub const CHATEDIT_CURSOR_DESCENT: f32 = FONTSIZE * 0.35;
+    pub const CHATEDIT_SELECT_ASCENT: f32 = 30.;
+    pub const CHATEDIT_SELECT_DESCENT: f32 = 10.;
+    pub const CHATEDIT_HANDLE_DESCENT: f32 = 35.;
+    pub const ACTION_PADDING: f32 = 8.;
+    pub const ACTION_SPACING: f32 = 4.;
+    pub const HEADER_HEIGHT: f32 = 60.;
+    pub const CONTENT_MARGIN: f32 = 15.;
+    pub const BACKARROW_SCALE: f32 = 15.;
+    pub const BACKARROW_X: f32 = 38.;
+    pub const BACKARROW_Y: f32 = 26.;
+    pub const BACKARROW_BG_W: f32 = 80.;
+    pub const COPY_WIDTH: f32 = 100.;
+    pub const COPY_SCALE: f32 = 15.;
+    pub const COPY_BTN_SIZE: f32 = CHATEDIT_HEIGHT;
+    pub const CONTENT_OUTLINE_SIZE: f32 = 0.3;
+}
+
+pub async fn make(
+    app: &App,
+    content: SceneNodePtr,
+    i18n_fish: &I18nBabelFish,
+    window_scale: PropertyFloat32,
+    contact_is_visible: PropertyBool,
+    channel_is_visible: PropertyBool,
+) -> SceneNodePtr {
+    let mut cc = expr::Compiler::new();
+    cc.add_const_f32("CHATEDIT_PAD", CHATEDIT_PAD);
+    cc.add_const_f32("CHATEDIT_HEIGHT", CHATEDIT_HEIGHT);
+    cc.add_const_f32("LABEL_LINESPACE", LABEL_LINESPACE);
+    cc.add_const_f32("HEADER_HEIGHT", HEADER_HEIGHT);
+    cc.add_const_f32("CONTENT_MARGIN", CONTENT_MARGIN);
+    cc.add_const_f32("CONTENT_OUTLINE_SIZE", CONTENT_OUTLINE_SIZE);
+    cc.add_const_f32("MENU_BTN_W_L", MENU_BTN_W_L);
+    cc.add_const_f32("COPY_WIDTH", COPY_WIDTH);
+    cc.add_const_f32("COPY_BTN_SIZE", COPY_BTN_SIZE);
+    cc.add_const_f32("CHANNEL_ITEM_HEIGHT", CHANNEL_ITEM_HEIGHT);
+
+    let renderer = app.renderer.clone();
+    let atom = &mut renderer.make_guard(gfxtag!("write_click"));
+
+    // Header
+    let node = create_vector_art("header_bg");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.set_f32(atom, Role::App, 3, HEADER_HEIGHT).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+
+    let (bg_color, sep_color) = match COLOR_SCHEME {
+        ColorScheme::DarkMode => ([0., 0., 0., 1.], [0.41, 0.6, 0.65, 1.]),
+        ColorScheme::PaperLight => ([1., 1., 1., 1.], [0., 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 + 1.),
+        expr::load_var("h"),
+        sep_color,
+    );
+    shape.add_outline(
+        expr::const_f32(0.),
+        expr::load_var("h"),
+        expr::load_var("w"),
+        cc.compile("h + 1").unwrap(),
+        CONTENT_OUTLINE_SIZE,
+        sep_color,
+    );
+    let color1 = [0., 0.17, 0.18, 0.3];
+    let color2 = [0., 0.88, 1., 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,
+    );
+
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    content.link(node);
+
+    // Create back arrow
+    let node = create_vector_art("back_btn_bg");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, BACKARROW_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, BACKARROW_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, BACKARROW_SCALE).unwrap();
+    prop.set_f32(atom, Role::App, 3, BACKARROW_SCALE).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
+
+    let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    content.link(node);
+
+    // Create back button
+    let node = create_button("back_btn");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 10).unwrap();
+    node.set_property_u32(atom, Role::App, "priority", 10).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 2, BACKARROW_BG_W).unwrap();
+    prop.set_f32(atom, Role::App, 3, HEADER_HEIGHT).unwrap();
+
+    let sg_root = app.sg_root.clone();
+    let contact_vis = contact_is_visible.clone();
+    let channel_vis = channel_is_visible.clone();
+    let renderer = app.renderer.clone();
+    let menu_node = sg_root.lookup_node("/window/content/menu_layer").unwrap();
+    let netstatus_layer = sg_root.lookup_node("/window/content/netstatus_layer").unwrap();
+    let goback = async move || {
+        info!(target: "app::chat", "clicked back");
+        let atom = &mut renderer.make_guard(gfxtag!("go back action"));
+
+        menu_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
+        netstatus_layer.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
+
+        contact_vis.set(atom, false);
+        channel_vis.set(atom, false);
+    };
+
+    let (slot, recvr) = Slot::new("back_clicked");
+    node.register("click", slot).unwrap();
+    let goback2 = goback.clone();
+    let listen_click = app.ex.spawn(async move {
+        while let Ok(_) = recvr.recv().await {
+            goback2().await;
+        }
+    });
+    content.push_task(listen_click);
+
+    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    content.link(node);
+
+    // Create shortcut to go back as well
+    let node = create_shortcut("back_shortcut");
+    #[cfg(target_os = "android")]
+    node.set_property_str(atom, Role::App, "key", "back").unwrap();
+    #[cfg(target_os = "macos")]
+    node.set_property_str(atom, Role::App, "key", "logo+left").unwrap();
+    #[cfg(all(not(target_os = "android"), not(target_os = "macos")))]
+    node.set_property_str(atom, Role::App, "key", "alt+left").unwrap();
+    // Not sure what was eating my keys. This is a workaround.
+    node.set_property_u32(atom, Role::App, "priority", 10).unwrap();
+
+    let (slot, recvr) = Slot::new("back_pressed");
+    node.register("shortcut", slot).unwrap();
+    let listen_enter = app.ex.spawn(async move {
+        while let Ok(_) = recvr.recv().await {
+            goback().await;
+        }
+    });
+    content.push_task(listen_enter);
+
+    let node = node.setup(|me| Shortcut::new(me)).await;
+    content.link(node);
+
+    let content_area_node = create_layer("content_area");
+    let prop = content_area_node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, CONTENT_MARGIN).unwrap();
+    prop.set_f32(atom, Role::App, 1, HEADER_HEIGHT + CONTENT_MARGIN).unwrap();
+    prop.set_expr(atom, Role::App, 2, cc.compile("w - 2. * CONTENT_MARGIN").unwrap()).unwrap();
+    prop.set_expr(
+        atom,
+        Role::App,
+        3,
+        cc.compile("h - HEADER_HEIGHT - CONTENT_MARGIN - 25.").unwrap(),
+    )
+    .unwrap();
+    content_area_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
+    content_area_node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
+    let content_area = content_area_node.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    content.link(content_area.clone());
+
+    // Red bottom glow below outline
+    let node = create_vector_art("bottom_glow");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, CONTENT_MARGIN).unwrap();
+    prop.set_expr(atom, Role::App, 1, cc.compile("h - 15").unwrap()).unwrap();
+    prop.set_expr(atom, Role::App, 2, cc.compile("w - 2. * CONTENT_MARGIN").unwrap()).unwrap();
+    prop.set_f32(atom, Role::App, 3, 15.).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
+    let mut shape = VectorShape::new();
+
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    content.link(node);
+
+    // Channels label bg
+    let node = create_vector_art("channels_label_bg");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.set_f32(atom, Role::App, 3, LABEL_LINESPACE).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
+
+    let mut shape = VectorShape::new();
+
+    let x1 = expr::const_f32(0.);
+    let y1 = expr::const_f32(0.);
+    let x2 = expr::load_var("w");
+    let y2 = cc.compile("LABEL_LINESPACE").unwrap();
+    let (color1, color2) = match COLOR_SCHEME {
+        ColorScheme::DarkMode => ([0., 0.11, 0.11, 0.], [0., 0., 0., 0.]),
+        ColorScheme::PaperLight => ([1., 1., 1., 0.], [1., 1., 1., 0.]),
+    };
+    let mut verts = vec![
+        ShapeVertex::new(x1.clone(), y1.clone(), color1),
+        ShapeVertex::new(x2.clone(), y1.clone(), color1),
+        ShapeVertex::new(x1.clone(), y2.clone(), color2),
+        ShapeVertex::new(x2, y2, color2),
+    ];
+    let mut indices = vec![0, 2, 1, 1, 2, 3];
+    shape.verts.append(&mut verts);
+    shape.indices.append(&mut indices);
+
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    content_area.link(node);
+
+    let node = create_vector_art("active_tab_overlay");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.set_f32(atom, Role::App, 3, LABEL_LINESPACE).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
+
+    let mut shape = VectorShape::new();
+    shape.add_outline(
+        cc.compile("w / 2").unwrap(),
+        expr::const_f32(0.),
+        cc.compile("w / 2 + CONTENT_OUTLINE_SIZE").unwrap(),
+        cc.compile("LABEL_LINESPACE").unwrap(),
+        CONTENT_OUTLINE_SIZE,
+        COLOR_CYAN,
+    );
+    shape.add_outline(
+        cc.compile("w / 2").unwrap(),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        cc.compile("CONTENT_OUTLINE_SIZE").unwrap(),
+        CONTENT_OUTLINE_SIZE,
+        COLOR_CYAN,
+    );
+    shape.add_outline(
+        cc.compile("w - CONTENT_OUTLINE_SIZE").unwrap(),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        cc.compile("LABEL_LINESPACE").unwrap(),
+        CONTENT_OUTLINE_SIZE,
+        COLOR_CYAN,
+    );
+
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    content_area.link(node);
+
+    let node = create_vector_art("active_tab_bg");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.set_f32(atom, Role::App, 3, LABEL_LINESPACE).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
+
+    let mut shape = VectorShape::new();
+    shape.add_filled_box(
+        cc.compile("w / 2").unwrap(),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        cc.compile("LABEL_LINESPACE").unwrap(),
+        [0., 0., 0., 0.5],
+    );
+
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    content_area.link(node);
+
+    let node = create_vector_art("inactive_tab_overlay");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.set_f32(atom, Role::App, 3, LABEL_LINESPACE).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.),
+        cc.compile("w / 2").unwrap(),
+        cc.compile("LABEL_LINESPACE").unwrap(),
+        [0.3, 0.3, 0.3, 0.2],
+    );
+    shape.add_outline(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        cc.compile("CONTENT_OUTLINE_SIZE").unwrap(),
+        cc.compile("LABEL_LINESPACE").unwrap(),
+        CONTENT_OUTLINE_SIZE,
+        COLOR_INACTIVE,
+    );
+    shape.add_outline(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        cc.compile("w / 2").unwrap(),
+        cc.compile("CONTENT_OUTLINE_SIZE").unwrap(),
+        CONTENT_OUTLINE_SIZE,
+        COLOR_INACTIVE,
+    );
+    shape.add_outline(
+        cc.compile("w / 2 - CONTENT_OUTLINE_SIZE").unwrap(),
+        expr::const_f32(0.),
+        cc.compile("w / 2").unwrap(),
+        cc.compile("LABEL_LINESPACE").unwrap(),
+        CONTENT_OUTLINE_SIZE,
+        COLOR_INACTIVE,
+    );
+
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    content_area.link(node);
+
+    let node = create_vector_art("input_area_bg");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, LABEL_LINESPACE).unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    let code = cc.compile("8. * CHATEDIT_PAD + 4. * CHATEDIT_HEIGHT").unwrap();
+    prop.set_expr(atom, Role::App, 3, code).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
+    node.set_property_u32(atom, Role::App, "priority", 0).unwrap();
+
+    let mut shape = VectorShape::new();
+    shape.add_filled_box(
+        cc.compile("1").unwrap(),
+        expr::const_f32(0.),
+        cc.compile("w - 1").unwrap(),
+        expr::load_var("h"),
+        [0., 0., 0., 0.5],
+    );
+
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    content_area.link(node);
+
+    let node = create_vector_art("fullscreen_label_bg");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
+
+    let mut shape = VectorShape::new();
+    shape.add_outline(
+        cc.compile("w - CONTENT_OUTLINE_SIZE").unwrap(),
+        cc.compile("LABEL_LINESPACE").unwrap(),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        CONTENT_OUTLINE_SIZE,
+        COLOR_CYAN,
+    );
+    shape.add_outline(
+        expr::const_f32(0.),
+        cc.compile("h - CONTENT_OUTLINE_SIZE").unwrap(),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        CONTENT_OUTLINE_SIZE,
+        COLOR_CYAN,
+    );
+    shape.add_outline(
+        expr::const_f32(0.),
+        cc.compile("LABEL_LINESPACE").unwrap(),
+        cc.compile("CONTENT_OUTLINE_SIZE").unwrap(),
+        expr::load_var("h"),
+        CONTENT_OUTLINE_SIZE,
+        COLOR_CYAN,
+    );
+    shape.add_outline(
+        cc.compile("w - CONTENT_OUTLINE_SIZE").unwrap(),
+        cc.compile("LABEL_LINESPACE").unwrap(),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        CONTENT_OUTLINE_SIZE,
+        COLOR_CYAN,
+    );
+    shape.add_outline(
+        expr::const_f32(0.),
+        cc.compile("LABEL_LINESPACE").unwrap(),
+        cc.compile("w / 2").unwrap(),
+        cc.compile("LABEL_LINESPACE + CONTENT_OUTLINE_SIZE").unwrap(),
+        CONTENT_OUTLINE_SIZE,
+        COLOR_CYAN,
+    );
+    // second horizontal line + gradient
+    shape.add_outline(
+        expr::const_f32(0.),
+        cc.compile("LABEL_LINESPACE + 5. * CHATEDIT_PAD + 3. * CHATEDIT_HEIGHT").unwrap(),
+        expr::load_var("w"),
+        cc.compile(
+            "LABEL_LINESPACE + 5. * CHATEDIT_PAD + 3. * CHATEDIT_HEIGHT + CONTENT_OUTLINE_SIZE",
+        )
+        .unwrap(),
+        CONTENT_OUTLINE_SIZE,
+        COLOR_CYAN,
+    );
+
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    content_area.link(node);
+
+    let node = create_button("channels_tab_btn");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
+    node.set_property_u32(atom, Role::App, "priority", 2).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    let code = cc.compile("w / 2").unwrap();
+    prop.set_expr(atom, Role::App, 0, code).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    let code = cc.compile("w").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, LABEL_LINESPACE).unwrap();
+
+    let (slot, recvr) = Slot::new("channelsbtn_clicked");
+    node.register("click", slot).unwrap();
+    let sg_root = app.sg_root.clone();
+    let contact_vis = contact_is_visible.clone();
+    let channel_vis = channel_is_visible.clone();
+    let renderer = app.renderer.clone();
+    let listen_click = app.ex.spawn(async move {
+        while let Ok(_) = recvr.recv().await {
+            let atom = &mut renderer.make_guard(gfxtag!("channels_click"));
+            contact_vis.set(atom, false);
+            channel_vis.set(atom, true);
+
+            let netstatus_layer = sg_root.lookup_node("/window/content/netstatus_layer").unwrap();
+            netstatus_layer.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
+
+            debug!("channels btn click - switching to channels");
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_click);
+
+    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    content_area.link(node);
+
+    let node = create_text("channels_tab_text");
+    let prop = node.get_property("rect").unwrap();
+    #[cfg(any(target_os = "android", feature = "emulate-android"))]
+    {
+    let code = cc.compile("w / 2 + CONTENT_MARGIN * 3.0").unwrap();
+    prop.set_expr(atom, Role::App, 0, code).unwrap();
+    prop.set_f32(atom, Role::App, 1, CONTENT_MARGIN * 1.4).unwrap();
+    let code = cc.compile("w").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, 40.).unwrap();
+    }
+    #[cfg(not(any(target_os = "android", feature = "emulate-android")))]
+    {
+    let code = cc.compile("w / 2 + CONTENT_MARGIN * 3.0").unwrap();
+    prop.set_expr(atom, Role::App, 0, code).unwrap();
+    prop.set_f32(atom, Role::App, 1, CONTENT_MARGIN * 1.15).unwrap();
+    let code = cc.compile("w").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, 40.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", LABEL_FONTSIZE).unwrap();
+    node.set_property_str(atom, Role::App, "text", "CHANNELS").unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, COLOR_MINT[0]).unwrap();
+    prop.set_f32(atom, Role::App, 1, COLOR_MINT[1]).unwrap();
+    prop.set_f32(atom, Role::App, 2, COLOR_MINT[2]).unwrap();
+    prop.set_f32(atom, Role::App, 3, COLOR_MINT[3]).unwrap();
+
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    content_area.link(node);
+
+    let node = create_button("contacts_tab_btn");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
+    node.set_property_u32(atom, Role::App, "priority", 2).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    let code = cc.compile("w / 2").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, LABEL_LINESPACE).unwrap();
+
+    let (slot, recvr) = Slot::new("contactsbtn_clicked");
+    node.register("click", slot).unwrap();
+    let sg_root = app.sg_root.clone();
+    let contact_vis = contact_is_visible.clone();
+    let channel_vis = channel_is_visible.clone();
+    let renderer = app.renderer.clone();
+    let listen_click = app.ex.spawn(async move {
+        while let Ok(_) = recvr.recv().await {
+            let atom = &mut renderer.make_guard(gfxtag!("contacts_click"));
+            channel_vis.set(atom, false);
+            contact_vis.set(atom, true);
+
+            let netstatus_layer = sg_root.lookup_node("/window/content/netstatus_layer").unwrap();
+            netstatus_layer.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
+
+            debug!("contacts btn click - switching to contacts");
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_click);
+
+    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    content_area.link(node);
+
+    let node = create_text("contacts_tab_text");
+    let prop = node.get_property("rect").unwrap();
+    #[cfg(any(target_os = "android", feature = "emulate-android"))]
+    {
+    prop.set_f32(atom, Role::App, 0, CONTENT_MARGIN * 3.0).unwrap();
+    prop.set_f32(atom, Role::App, 1, CONTENT_MARGIN * 1.4).unwrap();
+    prop.set_f32(atom, Role::App, 2, 200.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 40.).unwrap();
+    }
+    #[cfg(not(any(target_os = "android", feature = "emulate-android")))]
+    {
+    prop.set_f32(atom, Role::App, 0, CONTENT_MARGIN * 3.0).unwrap();
+    prop.set_f32(atom, Role::App, 1, CONTENT_MARGIN * 1.15).unwrap();
+    prop.set_f32(atom, Role::App, 2, 200.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 40.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", LABEL_FONTSIZE).unwrap();
+    node.set_property_str(atom, Role::App, "text", "CONTACTS").unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, COLOR_INACTIVE[0]).unwrap();
+    prop.set_f32(atom, Role::App, 1, COLOR_INACTIVE[1]).unwrap();
+    prop.set_f32(atom, Role::App, 2, COLOR_INACTIVE[2]).unwrap();
+    prop.set_f32(atom, Role::App, 3, COLOR_INACTIVE[3]).unwrap();
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    content_area.link(node);
+
+    let node = create_singleline_edit("channel_search");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    node.set_property_bool(atom, Role::App, "is_focused", false).unwrap();
+
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, CHATEDIT_PAD).unwrap();
+    prop.set_f32(atom, Role::App, 1, LABEL_LINESPACE + 7. * CHATEDIT_PAD + 3. * CHATEDIT_HEIGHT)
+        .unwrap();
+    let code = cc.compile("parent_w - 2 * CHATEDIT_PAD").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, CHATEDIT_HEIGHT).unwrap();
+
+    let prop = node.get_property("padding").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 2, TEXTBAR_BASELINE / 2.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 15.).unwrap();
+    node.set_property_f32(atom, Role::App, "baseline", TEXTBAR_BASELINE).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", FONTSIZE * 0.88).unwrap();
+
+    let prop = node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::PaperLight {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    let prop = node.get_property("text_hi_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.44).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.96).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    let prop = node.get_property("text_cmd_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.64).unwrap();
+    prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 2, 0.83).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    let prop = node.get_property("cursor_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.816).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.627).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    node.set_property_f32(atom, Role::App, "cursor_ascent", CHATEDIT_CURSOR_ASCENT).unwrap();
+    node.set_property_f32(atom, Role::App, "cursor_descent", CHATEDIT_CURSOR_DESCENT).unwrap();
+    node.set_property_f32(atom, Role::App, "select_ascent", CHATEDIT_SELECT_ASCENT).unwrap();
+    node.set_property_f32(atom, Role::App, "select_descent", CHATEDIT_SELECT_DESCENT).unwrap();
+    node.set_property_f32(atom, Role::App, "handle_descent", CHATEDIT_HANDLE_DESCENT).unwrap();
+    node.set_property_f32(atom, Role::App, "action_padding", ACTION_PADDING).unwrap();
+    node.set_property_f32(atom, Role::App, "action_spacing", ACTION_SPACING).unwrap();
+    let prop = node.get_property("hi_bg_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::PaperLight {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 0.8).unwrap();
+    } else if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 0.027).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.039).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.039).unwrap();
+        prop.set_f32(atom, Role::App, 3, 0.6).unwrap();
+    }
+    let prop = node.get_property("cmd_bg_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::PaperLight {
+        prop.set_f32(atom, Role::App, 0, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.30).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.25).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 6).unwrap();
+    node.set_property_u32(atom, Role::App, "priority", 3).unwrap();
+    //node.set_property_bool(atom, Role::App, "debug", true).unwrap();
+
+    node.set_property_str(atom, Role::App, "placeholder_text", "search").unwrap();
+    let prop = node.get_property("placeholder_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, COLOR_MINT_OP[0]).unwrap();
+    prop.set_f32(atom, Role::App, 1, COLOR_MINT_OP[1]).unwrap();
+    prop.set_f32(atom, Role::App, 2, COLOR_MINT_OP[2]).unwrap();
+    prop.set_f32(atom, Role::App, 3, COLOR_MINT_OP[3]).unwrap();
+
+    let node = node
+        .setup(|me| {
+            BaseEdit::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                BaseEditType::SingleLine,
+                app.ex.clone(),
+            )
+        })
+        .await;
+    let search_node = node.clone();
+    content_area.link(node);
+
+    let node = create_vector_art("search_bg");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, CHATEDIT_PAD).unwrap();
+    prop.set_f32(atom, Role::App, 1, LABEL_LINESPACE + 7. * CHATEDIT_PAD + 3. * CHATEDIT_HEIGHT)
+        .unwrap();
+    let code = cc.compile("w - 2 * CHATEDIT_PAD").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, CHATEDIT_HEIGHT).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 4).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., 0.5],
+    );
+
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    content_area.link(node);
+
+    let node = create_vector_art("search_outline");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, CHATEDIT_PAD).unwrap();
+    prop.set_f32(atom, Role::App, 1, LABEL_LINESPACE + 7. * CHATEDIT_PAD + 3. * CHATEDIT_HEIGHT)
+        .unwrap();
+    let code = cc.compile("w - 2 * CHATEDIT_PAD").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, CHATEDIT_HEIGHT).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 7).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"),
+        0.5,
+        [0.3, 0.3, 0.3, 1.],
+    );
+
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    content_area.link(node);
+
+    let node = create_singleline_edit("nick_edit");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    node.set_property_bool(atom, Role::App, "is_focused", false).unwrap();
+
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, CHATEDIT_PAD).unwrap();
+    prop.set_f32(atom, Role::App, 1, LABEL_LINESPACE + CHATEDIT_PAD).unwrap();
+    let code = cc.compile("parent_w - 2 * CHATEDIT_PAD").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, CHATEDIT_HEIGHT).unwrap();
+
+    let prop = node.get_property("padding").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 2, TEXTBAR_BASELINE / 2.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 15.).unwrap();
+    node.set_property_f32(atom, Role::App, "baseline", TEXTBAR_BASELINE).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", FONTSIZE * 0.88).unwrap();
+
+    let prop = node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::PaperLight {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    let prop = node.get_property("text_hi_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.44).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.96).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    let prop = node.get_property("text_cmd_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.64).unwrap();
+    prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 2, 0.83).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    let prop = node.get_property("cursor_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.816).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.627).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    node.set_property_f32(atom, Role::App, "cursor_ascent", CHATEDIT_CURSOR_ASCENT).unwrap();
+    node.set_property_f32(atom, Role::App, "cursor_descent", CHATEDIT_CURSOR_DESCENT).unwrap();
+    node.set_property_f32(atom, Role::App, "select_ascent", CHATEDIT_SELECT_ASCENT).unwrap();
+    node.set_property_f32(atom, Role::App, "select_descent", CHATEDIT_SELECT_DESCENT).unwrap();
+    node.set_property_f32(atom, Role::App, "handle_descent", CHATEDIT_HANDLE_DESCENT).unwrap();
+    node.set_property_f32(atom, Role::App, "action_padding", ACTION_PADDING).unwrap();
+    node.set_property_f32(atom, Role::App, "action_spacing", ACTION_SPACING).unwrap();
+    let prop = node.get_property("hi_bg_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::PaperLight {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 0.8).unwrap();
+    } else if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 0.027).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.039).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.039).unwrap();
+        prop.set_f32(atom, Role::App, 3, 0.6).unwrap();
+    }
+    let prop = node.get_property("cmd_bg_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::PaperLight {
+        prop.set_f32(atom, Role::App, 0, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.30).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.25).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 5).unwrap();
+    node.set_property_u32(atom, Role::App, "priority", 2).unwrap();
+    //node.set_property_bool(atom, Role::App, "debug", true).unwrap();
+
+    node.set_property_str(atom, Role::App, "placeholder_text", "#NAME").unwrap();
+    let prop = node.get_property("placeholder_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, COLOR_MINT_OP[0]).unwrap();
+    prop.set_f32(atom, Role::App, 1, COLOR_MINT_OP[1]).unwrap();
+    prop.set_f32(atom, Role::App, 2, COLOR_MINT_OP[2]).unwrap();
+    prop.set_f32(atom, Role::App, 3, COLOR_MINT_OP[3]).unwrap();
+
+    let node = node
+        .setup(|me| {
+            BaseEdit::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                BaseEditType::SingleLine,
+                app.ex.clone(),
+            )
+        })
+        .await;
+    let nickedit_node = node.clone();
+    content_area.link(node);
+
+    let node = create_vector_art("nick_bg");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, CHATEDIT_PAD).unwrap();
+    prop.set_f32(atom, Role::App, 1, LABEL_LINESPACE + CHATEDIT_PAD).unwrap();
+    let code = cc.compile("w - 2 * CHATEDIT_PAD").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, CHATEDIT_HEIGHT).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 4).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., 0.5],
+    );
+
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    content_area.link(node);
+
+    let node = create_vector_art("nick_outline");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, CHATEDIT_PAD).unwrap();
+    prop.set_f32(atom, Role::App, 1, LABEL_LINESPACE + CHATEDIT_PAD).unwrap();
+    let code = cc.compile("w - 2 * CHATEDIT_PAD").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, CHATEDIT_HEIGHT).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 7).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"),
+        CONTENT_OUTLINE_SIZE,
+        [0.3, 0.3, 0.3, 1.],
+    );
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    content_area.link(node);
+
+    let node = create_singleline_edit("secret_edit");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    node.set_property_bool(atom, Role::App, "is_focused", false).unwrap();
+
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, CHATEDIT_PAD).unwrap();
+    prop.set_f32(atom, Role::App, 1, LABEL_LINESPACE + 2. * CHATEDIT_PAD + CHATEDIT_HEIGHT)
+        .unwrap();
+    let code = cc.compile("parent_w - 2 * CHATEDIT_PAD").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, CHATEDIT_HEIGHT).unwrap();
+
+    let prop = node.get_property("padding").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 2, TEXTBAR_BASELINE / 2.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 15.).unwrap();
+    node.set_property_f32(atom, Role::App, "baseline", TEXTBAR_BASELINE).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", FONTSIZE * 0.88).unwrap();
+
+    let prop = node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::PaperLight {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    let prop = node.get_property("text_hi_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.44).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.96).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    let prop = node.get_property("text_cmd_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.64).unwrap();
+    prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 2, 0.83).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    let prop = node.get_property("cursor_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.816).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.627).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    node.set_property_f32(atom, Role::App, "cursor_ascent", CHATEDIT_CURSOR_ASCENT).unwrap();
+    node.set_property_f32(atom, Role::App, "cursor_descent", CHATEDIT_CURSOR_DESCENT).unwrap();
+    node.set_property_f32(atom, Role::App, "select_ascent", CHATEDIT_SELECT_ASCENT).unwrap();
+    node.set_property_f32(atom, Role::App, "select_descent", CHATEDIT_SELECT_DESCENT).unwrap();
+    node.set_property_f32(atom, Role::App, "handle_descent", CHATEDIT_HANDLE_DESCENT).unwrap();
+    node.set_property_f32(atom, Role::App, "action_padding", ACTION_PADDING).unwrap();
+    node.set_property_f32(atom, Role::App, "action_spacing", ACTION_SPACING).unwrap();
+    let prop = node.get_property("hi_bg_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::PaperLight {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 0.8).unwrap();
+    } else if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 0.027).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.039).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.039).unwrap();
+        prop.set_f32(atom, Role::App, 3, 0.6).unwrap();
+    }
+    let prop = node.get_property("cmd_bg_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::PaperLight {
+        prop.set_f32(atom, Role::App, 0, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.30).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.25).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 6).unwrap();
+    node.set_property_u32(atom, Role::App, "priority", 3).unwrap();
+    //node.set_property_bool(atom, Role::App, "debug", true).unwrap();
+
+    node.set_property_str(atom, Role::App, "placeholder_text", "KEY (OPTIONAL)").unwrap();
+    let prop = node.get_property("placeholder_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, COLOR_MINT_OP[0]).unwrap();
+    prop.set_f32(atom, Role::App, 1, COLOR_MINT_OP[1]).unwrap();
+    prop.set_f32(atom, Role::App, 2, COLOR_MINT_OP[2]).unwrap();
+    prop.set_f32(atom, Role::App, 3, COLOR_MINT_OP[3]).unwrap();
+
+    let node = node
+        .setup(|me| {
+            BaseEdit::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                BaseEditType::SingleLine,
+                app.ex.clone(),
+            )
+        })
+        .await;
+    let secedit_node = node.clone();
+    content_area.link(node);
+
+    let node = create_vector_art("secret_bg");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, CHATEDIT_PAD).unwrap();
+    prop.set_f32(atom, Role::App, 1, LABEL_LINESPACE + 2. * CHATEDIT_PAD + CHATEDIT_HEIGHT)
+        .unwrap();
+    let code = cc.compile("w - 2 * CHATEDIT_PAD").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, CHATEDIT_HEIGHT).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 4).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., 0.5],
+    );
+
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    content_area.link(node);
+
+    let node = create_vector_art("secret_outline");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, CHATEDIT_PAD).unwrap();
+    prop.set_f32(atom, Role::App, 1, LABEL_LINESPACE + 2. * CHATEDIT_PAD + CHATEDIT_HEIGHT)
+        .unwrap();
+    let code = cc.compile("w - 2 * CHATEDIT_PAD").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, CHATEDIT_HEIGHT).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 7).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"),
+        CONTENT_OUTLINE_SIZE,
+        [0.3, 0.3, 0.3, 1.],
+    );
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    content_area.link(node);
+
+    let node = create_vector_art("receive_copy_btn_bg");
+    let prop = node.get_property("rect").unwrap();
+    let code = cc.compile("w - CHATEDIT_PAD - COPY_BTN_SIZE / 2.").unwrap();
+    prop.set_expr(atom, Role::App, 0, code).unwrap();
+    prop.set_f32(
+        atom,
+        Role::App,
+        1,
+        LABEL_LINESPACE + 2. * CHATEDIT_PAD + CHATEDIT_HEIGHT + COPY_BTN_SIZE / 2.,
+    )
+    .unwrap();
+    prop.set_f32(atom, Role::App, 2, COPY_BTN_SIZE).unwrap();
+    prop.set_f32(atom, Role::App, 3, COPY_BTN_SIZE).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 8).unwrap();
+
+    let shape = shape::create_copy(COLOR_CYAN).scaled(COPY_SCALE);
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    content_area.link(node);
+
+    // paste clipboard into the KEY field
+    let node = create_button("receive_copy_btn");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    let code = cc.compile("w - CHATEDIT_PAD - COPY_BTN_SIZE").unwrap();
+    prop.set_expr(atom, Role::App, 0, code).unwrap();
+    prop.set_f32(atom, Role::App, 1, LABEL_LINESPACE + 2. * CHATEDIT_PAD + CHATEDIT_HEIGHT)
+        .unwrap();
+    prop.set_f32(atom, Role::App, 2, COPY_BTN_SIZE).unwrap();
+    prop.set_f32(atom, Role::App, 3, COPY_BTN_SIZE).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 9).unwrap();
+    node.set_property_u32(atom, Role::App, "priority", 5).unwrap();
+
+    let (slot, recvr) = Slot::new("receive_copy_clicked");
+    node.register("click", slot).unwrap();
+    let secedit_node2 = secedit_node.clone();
+    let renderer_clone = app.renderer.clone();
+    let listen_click = app.ex.spawn(async move {
+        while let Ok(_) = recvr.recv().await {
+            debug!(target: "app::menu", "secret paste button clicked");
+            match miniquad::window::clipboard_get() {
+                Some(clipboard_text) => {
+                    let text_prop = secedit_node2.get_property("text").unwrap();
+                    let atom = &mut renderer_clone.make_guard(gfxtag!("secret paste"));
+                    text_prop.set_str(atom, Role::App, 0, &clipboard_text).unwrap();
+                    if let crate::scene::Pimpl::Edit(edit) = secedit_node2.pimpl() {
+                        edit.on_text_prop_changed();
+                    }
+                }
+                None => warn!(target: "app::menu", "clipboard_get() returned None (empty or unsupported on this platform)"),
+            }
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_click);
+
+    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    content_area.link(node);
+
+    let node = create_layer("addchannel_btn_layer");
+    let prop = node.get_property("rect").unwrap();
+    let code = cc.compile("w - CHATEDIT_PAD - MENU_BTN_W_L - 45").unwrap();
+    prop.set_expr(atom, Role::App, 0, code).unwrap();
+    let code = cc.compile("LABEL_LINESPACE + 3. * CHATEDIT_PAD + 2. * CHATEDIT_HEIGHT").unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    let code = cc.compile("MENU_BTN_W_L + 45").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, CHATEDIT_HEIGHT * 0.95).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();
+    let editlayer_node = node.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    content_area.link(editlayer_node.clone());
+
+    let node = create_vector_art("btns_bg");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
+
+    let mut shape = VectorShape::new();
+    shape.add_outline(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::const_f32(MENU_BTN_W_L + 45.),
+        expr::load_var("h"),
+        CONTENT_OUTLINE_SIZE,
+        COLOR_CYAN,
+    );
+
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    editlayer_node.link(node);
+
+    let node = create_button("addchannel_btn");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    let code = cc.compile("MENU_BTN_W_L + 45").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, CHATEDIT_HEIGHT).unwrap();
+    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    editlayer_node.link(node);
+
+    let node = create_text("add_channel");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, BTN_TEXT_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, MENU_BTN_W_L + 45.).unwrap();
+    prop.set_f32(atom, Role::App, 3, CHATEDIT_HEIGHT).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", FONTSIZE * 0.95).unwrap();
+    node.set_property_str(atom, Role::App, "text", "add channel").unwrap();
+    let prop = node.get_property("text_align").unwrap();
+    prop.set_enum(atom, Role::App, 0, "center").unwrap();
+    node.set_property_bool(atom, Role::App, "use_i18n", false).unwrap();
+
+    let prop = node.get_property("text_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, COLOR_CYAN[0]).unwrap();
+    prop.set_f32(atom, Role::App, 1, COLOR_CYAN[1]).unwrap();
+    prop.set_f32(atom, Role::App, 2, COLOR_CYAN[2]).unwrap();
+    prop.set_f32(atom, Role::App, 3, COLOR_CYAN[3]).unwrap();
+
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    editlayer_node.link(node);
+
+    // Create cancel/done edit buttons at bottom of screen
+    let btns = edit_buttons::create_edit_buttons(
+        app, content.clone(), &window_scale, i18n_fish,
+    ).await;
+
+    let node = create_menu("nick_menu");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, LABEL_LINESPACE + 8. * CHATEDIT_PAD + 4. * CHATEDIT_HEIGHT)
+        .unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    let code =
+        cc.compile("h - (LABEL_LINESPACE + 8. * CHATEDIT_PAD + 4. * CHATEDIT_HEIGHT)").unwrap();
+    prop.set_expr(atom, Role::App, 3, code).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
+    node.set_property_u32(atom, Role::App, "priority", 0).unwrap();
+
+    let prop = node.get_property("bg_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 0.5).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", FONTSIZE).unwrap();
+    node.set_property_f32(atom, Role::App, "sep_size", MENU_SEP_SIZE).unwrap();
+
+    let prop = node.get_property("text_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+
+    let prop = node.get_property("active_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.36).unwrap();
+    prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 2, 0.51).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+
+    let prop = node.get_property("alert_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.56).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.61).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+
+    let prop = node.get_property("sep_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.4).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.4).unwrap();
+    prop.set_f32(atom, Role::App, 2, 0.4).unwrap();
+    prop.set_f32(atom, Role::App, 3, 0.4).unwrap();
+
+    let prop = node.get_property("padding").unwrap();
+    prop.set_f32(atom, Role::App, 0, LABEL_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, CHANNEL_ITEM_HEIGHT / 2.).unwrap();
+
+    node.set_property_f32(atom, Role::App, "handle_padding", MENU_HANDLE_PAD).unwrap();
+    node.set_property_f32(atom, Role::App, "fade_zone", MENU_FADE).unwrap();
+
+    let prop = node.get_property("items").unwrap();
+    for channel in [
+        "#general",
+        "#random",
+        "#dev",
+        "#announcements",
+        "#memes",
+        "#offtopic",
+        "#support",
+        "#trading",
+        "#newbies",
+        "#intro",
+    ] {
+        prop.push_str(atom, Role::App, channel).unwrap();
+    }
+
+    let menu_node =
+        node.setup(|me| Menu::new(me, window_scale.clone(), app.renderer.clone())).await;
+    content_area.link(menu_node.clone());
+
+    // Connect cancel/done buttons and edit_active signal
+    btns.connect_edit_handlers(app, &menu_node, None);
+
+    // Only one input field may be focused (caret visible)
+    edit_switch(
+        &mut app.tasks.lock().unwrap(),
+        &[search_node, nickedit_node, secedit_node],
+        app.ex.clone(),
+    );
+
+    content
+}