darkfi пре 1 година
родитељ
комит
345f9cbb2b

+ 26 - 0
bin/app/data/confirm.obj

@@ -0,0 +1,26 @@
+# Blender 4.0.2
+# www.blender.org
+mtllib confirm.mtl
+o Plane
+v -0.520000 0.000000 0.600000
+v -0.520000 0.000000 -0.600000
+v 0.500000 0.000000 0.000000
+v -1.050000 0.000000 1.500000
+v -1.050000 0.000000 -1.500000
+v 1.500000 0.000000 0.000000
+v -0.880000 0.000000 1.200000
+v -0.880000 0.000000 -1.200000
+v 1.160000 0.000000 0.000000
+vn -0.0000 1.0000 -0.0000
+vt 1.000000 0.000000
+vt 1.000000 1.000000
+vt 0.000000 1.000000
+vt 0.000000 0.000000
+s 0
+f 1/1/1 3/2/1 2/3/1
+f 6/4/1 7/4/1 4/4/1
+f 6/4/1 8/4/1 9/4/1
+f 4/4/1 8/4/1 5/4/1
+f 6/4/1 9/4/1 7/4/1
+f 6/4/1 5/4/1 8/4/1
+f 4/4/1 7/4/1 8/4/1

+ 126 - 0
bin/app/data/settings.obj

@@ -0,0 +1,126 @@
+# Blender 4.0.2
+# www.blender.org
+mtllib settings.mtl
+o Plane.006
+v -0.282843 0.000000 0.000000
+v 0.000000 0.000000 0.282843
+v -0.000000 0.000000 -0.282843
+v 0.282843 0.000000 -0.000000
+v -0.050000 0.000000 1.000000
+v 0.050000 0.000000 0.900000
+v -0.050000 0.000000 -0.900000
+v 0.050000 0.000000 -1.000000
+v -0.050000 0.000000 -0.335000
+v 0.050000 0.000000 -0.335000
+v 0.000000 0.000000 0.950000
+v -0.000000 0.000000 -0.950000
+v -0.000000 0.000000 -0.385000
+v -0.050000 0.000000 0.331500
+v 0.050000 0.000000 0.331500
+v 0.000000 0.000000 0.381500
+v -0.982843 0.000000 0.450000
+v -0.700000 0.000000 0.732843
+v -0.700000 0.000000 0.167157
+v -0.417157 0.000000 0.450000
+v -0.750000 0.000000 1.000000
+v -0.650000 0.000000 0.900000
+v -0.750000 0.000000 -0.900000
+v -0.650000 0.000000 -1.000000
+v -0.750000 0.000000 0.115000
+v -0.650000 0.000000 0.115000
+v -0.700000 0.000000 0.950000
+v -0.700000 0.000000 -0.950000
+v -0.700000 0.000000 0.065000
+v -0.750000 0.000000 0.781500
+v -0.650000 0.000000 0.781500
+v -0.700000 0.000000 0.831500
+v 0.417157 0.000000 -0.450000
+v 0.700000 0.000000 -0.167157
+v 0.700000 0.000000 -0.732843
+v 0.982843 0.000000 -0.450000
+v 0.650000 0.000000 1.000000
+v 0.750000 0.000000 0.900000
+v 0.650000 0.000000 -0.900000
+v 0.750000 0.000000 -1.000000
+v 0.650000 0.000000 -0.785000
+v 0.750000 0.000000 -0.785000
+v 0.700000 0.000000 0.950000
+v 0.700000 0.000000 -0.950000
+v 0.700000 0.000000 -0.835000
+v 0.650000 0.000000 -0.118500
+v 0.750000 0.000000 -0.118500
+v 0.700000 0.000000 -0.068500
+v -0.869706 0.000000 0.450000
+v -0.700000 0.000000 0.619706
+v -0.700000 0.000000 0.280294
+v -0.530294 0.000000 0.450000
+v 0.530294 0.000000 -0.450000
+v 0.700000 0.000000 -0.280294
+v 0.700000 0.000000 -0.619706
+v 0.869706 0.000000 -0.450000
+v -0.169706 0.000000 0.000000
+v 0.000000 0.000000 0.169706
+v -0.000000 0.000000 -0.169706
+v 0.169706 0.000000 -0.000000
+vn -0.0000 1.0000 -0.0000
+vt 0.000000 0.000000
+vt 1.000000 0.950000
+vt 0.500000 1.000000
+vt 0.500000 0.950000
+vt 0.000000 1.000000
+vt 0.000000 0.950000
+vt 0.500000 0.000000
+vt 0.000000 0.665000
+vt 1.000000 0.000000
+vt 0.500000 0.665000
+vt 1.000000 1.000000
+vt 1.000000 0.665000
+s 0
+f 1/1/1 59/1/1 3/1/1
+f 10/2/1 12/3/1 13/4/1
+f 13/4/1 7/5/1 9/6/1
+f 11/7/1 14/8/1 5/1/1
+f 6/9/1 16/10/1 11/7/1
+f 19/1/1 49/1/1 51/1/1
+f 29/4/1 24/11/1 28/3/1
+f 29/4/1 23/5/1 25/6/1
+f 27/7/1 30/8/1 21/1/1
+f 22/9/1 32/10/1 27/7/1
+f 34/1/1 53/1/1 33/1/1
+f 42/2/1 44/3/1 45/4/1
+f 45/4/1 39/5/1 41/6/1
+f 37/1/1 48/10/1 46/8/1
+f 38/9/1 48/10/1 43/7/1
+f 4/1/1 58/1/1 2/1/1
+f 10/2/1 8/11/1 12/3/1
+f 13/4/1 12/3/1 7/5/1
+f 11/7/1 16/10/1 14/8/1
+f 6/9/1 15/12/1 16/10/1
+f 20/1/1 50/1/1 18/1/1
+f 29/4/1 26/2/1 24/11/1
+f 29/4/1 28/3/1 23/5/1
+f 27/7/1 32/10/1 30/8/1
+f 22/9/1 31/12/1 32/10/1
+f 35/1/1 56/1/1 36/1/1
+f 42/2/1 40/11/1 44/3/1
+f 45/4/1 44/3/1 39/5/1
+f 37/1/1 43/7/1 48/10/1
+f 38/9/1 47/12/1 48/10/1
+f 19/1/1 52/1/1 20/1/1
+f 18/1/1 49/1/1 17/1/1
+f 34/1/1 56/1/1 54/1/1
+f 33/1/1 55/1/1 35/1/1
+f 3/1/1 60/1/1 4/1/1
+f 2/1/1 57/1/1 1/1/1
+f 1/1/1 57/1/1 59/1/1
+f 19/1/1 17/1/1 49/1/1
+f 34/1/1 54/1/1 53/1/1
+f 4/1/1 60/1/1 58/1/1
+f 20/1/1 52/1/1 50/1/1
+f 35/1/1 55/1/1 56/1/1
+f 19/1/1 51/1/1 52/1/1
+f 18/1/1 50/1/1 49/1/1
+f 34/1/1 36/1/1 56/1/1
+f 33/1/1 53/1/1 55/1/1
+f 3/1/1 59/1/1 60/1/1
+f 2/1/1 58/1/1 57/1/1

+ 26 - 0
bin/app/data/switch.obj

@@ -0,0 +1,26 @@
+# Blender 4.0.2
+# www.blender.org
+mtllib switch.mtl
+o Plane.001
+v -1.100000 0.000000 -0.500000
+v 1.700000 0.000000 -0.200000
+v -1.700000 0.000000 -0.200000
+v 0.900000 0.000000 -0.500000
+v 0.300000 0.000000 -1.400000
+v 1.100000 0.000000 0.500000
+v -1.700000 0.000000 0.200000
+v 1.700000 0.000000 0.200000
+v -0.900000 0.000000 0.500000
+v -0.300000 0.000000 1.400000
+vn -0.0000 1.0000 -0.0000
+vt 0.200000 0.000000
+vt 1.000000 1.000000
+vt 0.000000 1.000000
+vt 1.000000 0.000000
+s 0
+f 4/1/1 3/2/1 2/3/1
+f 9/1/1 8/2/1 7/3/1
+f 2/3/1 5/1/1 4/1/1
+f 4/1/1 1/4/1 3/2/1
+f 9/1/1 6/4/1 8/2/1
+f 7/3/1 10/1/1 9/1/1

+ 4 - 0
bin/app/gui/print_tree.py

@@ -49,6 +49,10 @@ def print_node_info(parent_path, depth, indent):
                 child_type = "chat_edit"
             case SceneNodeType.BUTTON:
                 child_type = "button"
+            case SceneNodeType.SETTING_ROOT:
+                child_type = "setting_root"
+            case SceneNodeType.SETTING:
+                child_type = "setting"
 
         desc = f"{ws}{child_name}:{child_id}/"
         desc += " "*(50 - len(desc))

+ 2 - 0
bin/app/pydrk/api.py

@@ -64,6 +64,8 @@ class SceneNodeType:
     CHAT_EDIT = 18
     IMAGE = 18
     BUTTON = 19
+    SETTING_ROOT = 24
+    SETTING = 25
 
 class PropertyType:
     NULL = 0

+ 4 - 3
bin/app/src/app/mod.rs

@@ -48,7 +48,7 @@ use crate::{
 mod node;
 use node::create_darkirc;
 mod schema;
-use schema::get_window_scale_filename;
+use schema::{get_window_scale_filename, settings};
 
 macro_rules! d { ($($arg:tt)*) => { debug!(target: "app", $($arg)*); } }
 macro_rules! t { ($($arg:tt)*) => { trace!(target: "app", $($arg)*); } }
@@ -173,8 +173,7 @@ impl App {
 
         let window = window.setup(|me| Window::new(me, self.render_api.clone())).await;
         self.sg_root.clone().link(window.clone());
-        schema::make(&self, window).await;
-        //schema::test::make(&self, window).await;
+        schema::make(&self, window.clone()).await;
 
         d!("Schema loaded");
 
@@ -186,6 +185,8 @@ impl App {
 
         #[cfg(not(feature = "enable-plugins"))]
         w!("Plugins are disabled in this build");
+
+        settings::make(&self, window).await;
     }
 
     #[cfg(feature = "enable-plugins")]

+ 2 - 0
bin/app/src/app/node.rs

@@ -297,6 +297,8 @@ pub fn create_editbox(name: &str) -> SceneNode {
     let prop = Property::new("debug", PropertyType::Bool, PropertySubType::Null);
     node.add_property(prop).unwrap();
 
+    node.add_signal("enter_pressed", "Enter key pressed", vec![]).unwrap();
+
     node
 }
 

+ 94 - 0
bin/app/src/app/schema/mod.rs

@@ -48,12 +48,14 @@ use crate::{
 
 mod chat;
 mod menu;
+pub mod settings;
 pub mod test;
 
 pub const COLOR_SCHEME: ColorScheme = ColorScheme::DarkMode;
 //pub const COLOR_SCHEME: ColorScheme = ColorScheme::PaperLight;
 
 mod android_ui_consts {
+    pub const SETTINGS_ICON_SIZE: f32 = 140.;
     pub const NETSTATUS_ICON_SIZE: f32 = 140.;
     pub const NETLOGO_SCALE: f32 = 50.;
     pub const EMOJI_PICKER_ICON_SIZE: f32 = 100.;
@@ -109,6 +111,8 @@ mod ui_consts {
     not(feature = "emulate-android")
 ))]
 mod ui_consts {
+    pub const SETTINGS_ICON_SIZE: f32 = 60.;
+    pub const SETTINGS_LOGO_SCALE: f32 = 25.;
     pub const NETSTATUS_ICON_SIZE: f32 = 60.;
     pub const NETLOGO_SCALE: f32 = 25.;
     pub const EMOJI_PICKER_ICON_SIZE: f32 = 40.;
@@ -129,6 +133,7 @@ enum ColorScheme {
 pub async fn make(app: &App, window: SceneNodePtr) {
     let mut cc = Compiler::new();
     cc.add_const_f32("NETSTATUS_ICON_SIZE", NETSTATUS_ICON_SIZE);
+    cc.add_const_f32("SETTINGS_ICON_SIZE", NETSTATUS_ICON_SIZE);
 
     let atom = &mut PropertyAtomicGuard::new();
 
@@ -396,6 +401,95 @@ pub async fn make(app: &App, window: SceneNodePtr) {
         node.setup(|me| VectorArt::new(me, shape, app.render_api.clone(), app.ex.clone())).await;
     netlayer_node.clone().link(net3_node);
 
+    // Navbar Settings Button
+
+    // Layer
+    let settingslayer_node = create_layer("settings_button_layer");
+    let prop = settingslayer_node.get_property("rect").unwrap();
+    let code = cc.compile("w - NETSTATUS_ICON_SIZE - SETTINGS_ICON_SIZE").unwrap();
+    prop.clone().set_expr(atom, Role::App, 0, code).unwrap();
+    prop.clone().set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.clone().set_f32(atom, Role::App, 2, 1000.).unwrap();
+    prop.clone().set_f32(atom, Role::App, 3, 1000.).unwrap();
+    settingslayer_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
+    settingslayer_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let settingslayer_node =
+        settingslayer_node.setup(|me| Layer::new(me, app.render_api.clone(), app.ex.clone())).await;
+    window.clone().link(settingslayer_node.clone());
+
+    // Background
+    let node = create_vector_art("settings_btn_bg");
+    let prop = node.get_property("rect").unwrap();
+    prop.clone().set_f32(atom, Role::App, 0, NETSTATUS_ICON_SIZE / 2.).unwrap();
+    prop.clone().set_f32(atom, Role::App, 1, NETSTATUS_ICON_SIZE / 2.).unwrap();
+    prop.clone().set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.clone().set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
+    node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
+    let shape = shape::create_settings([0., 0.94, 1., 1.]).scaled(20.);
+    let node =
+        node.setup(|me| VectorArt::new(me, shape, app.render_api.clone(), app.ex.clone())).await;
+    settingslayer_node.clone().link(node);
+
+    // Button
+    let node = create_button("settings_btn");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.clone().set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.clone().set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.clone().set_f32(atom, Role::App, 2, NETSTATUS_ICON_SIZE).unwrap();
+    prop.clone().set_f32(atom, Role::App, 3, NETSTATUS_ICON_SIZE).unwrap();
+
+    let sg_root = app.sg_root.clone();
+    let settings = move || {
+        info!(target: "app::chat", "clicked settings");
+        let atom = &mut PropertyAtomicGuard::new();
+
+        // Hide all relevant window children nodes
+        // Messy.
+        //
+        // Some suggestions:
+        //  1. Something closer to a router, that would be a accessible globally,
+        //  which essentially holds a vector of references to SceneNodes
+        //  representing the app navigation history.
+        //  When the user changes the route, it would make invisible (or later remove
+        //  elements from the tree for optimization purposes) the node of the last SceneNodes
+        //  in the vector and all its children, recursively;
+        //  and append a new SceneNode pointer, which is the new "route" chosen by the user,
+        //  and draw it and its children recursively.
+        //  Note that this would implicitly handle nested routes (like
+        //  /window/somewhere1/view1 to /window/somewhere1/view2, if the last element of the
+        //  router currently points to view1 and we call router.goto("./view2")).
+        //  
+        //  2. Support of wildcard in lookups in .get_children() or another method, like this "*_chat_layer".
+        let windows = sg_root.clone().lookup_node("/window").unwrap().get_children();
+        let target_substrings = vec!["_chat_layer", "menu_layer", "settings_layer"];
+        for node in windows.iter() {
+            if target_substrings.iter().any(|&s| node.name.contains(s)) {
+                if let Err(e) = node.set_property_bool(atom, Role::App, "is_visible", false) {
+                    debug!("Failed to set property 'is_visible' on node: {:?}", e);
+                }
+            }
+        }
+
+        // Show settings
+        let settings_node = sg_root.clone().lookup_node("/window/settings_layer").unwrap();
+        settings_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
+    };
+
+    let (slot, recvr) = Slot::new("settings_clicked");
+    node.register("click", slot).unwrap();
+    let settings2 = settings.clone();
+    let listen_click = app.ex.spawn(async move {
+        while let Ok(_) = recvr.recv().await {
+            settings2();
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_click);
+    
+    let node = node.setup(|me| Button::new(me, app.ex.clone())).await;
+    settingslayer_node.clone().link(node);
+
     let emoji_meshes = emoji_picker::EmojiMeshes::new(
         app.render_api.clone(),
         app.text_shaper.clone(),

+ 785 - 0
bin/app/src/app/schema/settings.rs

@@ -0,0 +1,785 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 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 sled_overlay::sled;
+
+
+use crate::{
+    app::{
+        node::{
+            create_button, create_editbox,
+            create_layer, create_text, create_vector_art,
+        },
+        App,
+    },
+    expr::{self, Compiler},
+    prop::{
+        PropertyAtomicGuard, PropertyFloat32, PropertyStr, Property, PropertyValue, Role,
+    },
+    scene::{SceneNodePtr, Slot},
+    shape,
+    ui::{
+        Button, EditBox, Layer, ShapeVertex, Text, VectorArt,
+        VectorShape,
+    },
+};
+
+use super::{ColorScheme, CHANNELS, COLOR_SCHEME};
+
+use std::collections::BTreeMap;
+use std::sync::{Arc, Mutex};
+
+mod android_ui_consts {
+    pub const SETTING_LABEL_X: f32 = 40.;
+    pub const SETTING_LABEL_LINESPACE: f32 = 140.;
+    pub const SETTING_LABEL_BASELINE: f32 = 82.;
+    pub const SUBTITLE_LABEL_FONTSIZE: f32 = 36.;
+    pub const SETTING_LABEL_FONTSIZE: f32 = 24.;
+    pub const SETTING_EDIT_FONTSIZE: f32 = 24.;
+    pub const SETTING_TITLE_X: f32 = 150.;
+    pub const SETTING_TITLE_FONTSIZE: f32 = 40.;
+    pub const SETTING_TITLE_BASELINE: f32 = 82.;
+    pub const BORDER_RIGHT_SCALE: f32 = 15.;
+
+    pub const BACKARROW_SCALE: f32 = 30.;
+    pub const BACKARROW_X: f32 = 50.;
+    pub const BACKARROW_Y: f32 = 70.;
+}
+
+#[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 SETTING_LABEL_X: f32 = 20.;
+    pub const SETTING_LABEL_LINESPACE: f32 = 60.;
+    pub const SETTING_LABEL_BASELINE: f32 = 37.;
+    pub const SUBTITLE_LABEL_FONTSIZE: f32 = 16.;
+    pub const SETTING_LABEL_FONTSIZE: f32 = 14.;
+    pub const SETTING_EDIT_FONTSIZE: f32 = 14.;
+    pub const SETTING_TITLE_X: f32 = 100.;
+    pub const SETTING_TITLE_FONTSIZE: f32 = 20.;
+    pub const SETTING_TITLE_BASELINE: f32 = 37.;
+    pub const BORDER_RIGHT_SCALE: f32 = 30.;
+
+    pub const BACKARROW_SCALE: f32 = 15.;
+    pub const BACKARROW_X: f32 = 38.;
+    pub const BACKARROW_Y: f32 = 26.;
+}
+
+use ui_consts::*;
+
+#[derive(Clone)]
+struct Setting {
+    name: String,
+    node: SceneNodePtr,
+}
+
+impl Setting {
+    fn value_as_string(&self) -> String {
+        match &self.node.get_property("value").unwrap().get_value(0).ok().unwrap() {
+            PropertyValue::Str(s) => s.clone(),
+            PropertyValue::Uint32(i) => i.to_string(),
+            PropertyValue::Bool(b) => {
+                if *b {
+                    "true".to_string()
+                } else {
+                    "false".to_string()
+                }
+            }
+            PropertyValue::Float32(fl) => fl.to_string(),
+            _ => "unknown".to_string(),
+        }
+    }
+    fn get_value(&self) -> PropertyValue {
+        self.node.get_property("value").unwrap().get_value(0).ok().unwrap()
+    }
+}
+
+pub async fn make(app: &App, window: SceneNodePtr) {
+    let mut cc = Compiler::new();
+    cc.add_const_f32("BORDER_RIGHT_SCALE", BORDER_RIGHT_SCALE);
+    cc.add_const_f32("X_RATIO", 1./3.);
+    let window_scale = PropertyFloat32::wrap(&window, Role::Internal, "scale", 0).unwrap();
+    let atom = &mut PropertyAtomicGuard::new();
+
+    // Main view
+    let layer_node = create_layer("settings_layer");
+    let prop = layer_node.get_property("rect").unwrap();
+    prop.clone().set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.clone().set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.clone().set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.clone().set_expr(atom, Role::App, 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();
+    let layer_node =
+        layer_node.setup(|me| Layer::new(me, app.render_api.clone(), app.ex.clone())).await;
+    window.clone().link(layer_node.clone());
+
+    let mut setting_y = 0.;
+
+    // Create the back button
+    let node = create_vector_art("back_btn_bg");
+    let prop = node.get_property("rect").unwrap();
+    prop.clone().set_f32(atom, Role::App, 0, BACKARROW_X).unwrap();
+    prop.clone().set_f32(atom, Role::App, 1, BACKARROW_Y).unwrap();
+    prop.clone().set_f32(atom, Role::App, 2, 0.).unwrap();
+    prop.clone().set_f32(atom, Role::App, 3, 0.).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.render_api.clone(), app.ex.clone())).await;
+    layer_node.clone().link(node);
+
+
+    let node = create_button("back_btn");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.clone().set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.clone().set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.clone().set_f32(atom, Role::App, 2, 100.).unwrap();
+    prop.clone().set_f32(atom, Role::App, 3, 100.).unwrap();
+
+    let sg_root = app.sg_root.clone();
+    let goback = move || {
+        let atom = &mut PropertyAtomicGuard::new();
+
+        // Disable visilibity of all relevant window nodes
+        // This is needed since for example all chats have a different node name.
+        let windows = sg_root.clone().lookup_node("/window").unwrap().get_children();
+        let target_substrings = vec!["_chat_layer", "menu_layer", "settings_layer"];
+
+        for node in windows.iter() {
+            // Check if the node's name contains any of the target substrings
+            if target_substrings.iter().any(|&s| node.name.contains(s)) {
+                if let Err(e) = node.set_property_bool(atom, Role::App, "is_visible", false) {
+                    debug!("Failed to set property 'is_visible' on node: {:?}", e);
+                }
+            }
+        }
+
+        // Go back to the dev channel
+        let menu_node = sg_root.clone().lookup_node("/window/dev_chat_layer").unwrap();
+        menu_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
+    };
+
+    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();
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_click);
+
+    let node = node.setup(|me| Button::new(me, app.ex.clone())).await;
+    layer_node.clone().link(node.clone());
+    
+    // Label: "SETTINGS" title
+    let node = create_text("settings_label_fontsize");
+    let prop = node.get_property("rect").unwrap();
+    prop.clone().set_f32(atom, Role::App, 0, SETTING_TITLE_X).unwrap();
+    prop.clone().set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.clone().set_f32(atom, Role::App, 2, 1000.).unwrap();
+    prop.clone().set_f32(atom, Role::App, 3, 200.).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
+    node.set_property_f32(atom, Role::App, "baseline", SETTING_TITLE_BASELINE).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", SETTING_TITLE_FONTSIZE).unwrap();
+    node.set_property_str(atom, Role::App, "text", "SETTINGS").unwrap();
+    //node.set_property_str(atom, Role::App, "text", "anon1").unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.clone().set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.clone().set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.clone().set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.clone().set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else if COLOR_SCHEME == ColorScheme::PaperLight {
+        prop.clone().set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.clone().set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.clone().set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.clone().set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
+
+    let node = node
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.render_api.clone(),
+                app.text_shaper.clone(),
+                app.ex.clone(),
+            )
+        })
+        .await;
+    layer_node.clone().link(node);
+
+    setting_y += 20.;
+
+    // Create a BTreeMap to store settings
+    let mut settings_map: BTreeMap<String, Arc<Setting>> = BTreeMap::new();
+
+    // Get the settings from all plugins
+    let sg_root_children = app.sg_root.clone().get_children();
+    let plugin_node = sg_root_children.iter().find(|node| node.name == "plugin");
+    if let Some(pnode) = plugin_node {
+        for plugin in pnode.get_children().iter() {
+            let plugin_children = plugin.get_children();
+            let setting_root = plugin_children.iter().find(|node| node.name == "setting");
+            if let Some(sroot) = setting_root {
+                for setting in sroot.get_children().iter() {
+                    let name = vec![plugin.name.clone(), setting.name.clone()].join(".");
+                    settings_map.insert(
+                        name.clone(),
+                        Arc::new(Setting {
+                            name,
+                            node: setting.clone(),
+                        }),
+                    );
+                }
+            }
+        }
+    }
+
+    // Setting currently being edited
+    let active_setting: Arc<Mutex<Option<Arc<Setting>>>> = Arc::new(Mutex::new(None));
+
+    // Iterate over the map and process each setting
+    for (key, setting) in settings_map {
+        let setting_clone = setting.clone();
+        let setting_name = setting_clone.name.clone();
+
+        setting_y += SETTING_LABEL_LINESPACE;
+
+        // Background Label
+        let node = create_vector_art("settings_label_bg");
+        let prop = node.get_property("rect").unwrap();
+        prop.clone().set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.clone().set_f32(atom, Role::App, 1, setting_y).unwrap();
+        prop.clone().set_expr(atom, Role::App, 2, cc.compile("w  * X_RATIO - BORDER_RIGHT_SCALE").unwrap()).unwrap();
+        prop.clone().set_f32(atom, Role::App, 3, SETTING_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 = expr::const_f32(SETTING_LABEL_LINESPACE);
+        let (color1, color2) = match COLOR_SCHEME {
+            ColorScheme::DarkMode => ([0., 0.11, 0.11, 1.], [0., 0.11, 0.11, 1.]),
+            ColorScheme::PaperLight => ([1., 1., 1., 1.], [1., 1., 1., 1.]),
+        };
+        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);
+
+        shape.add_filled_box(
+            expr::const_f32(0.),
+            expr::const_f32(SETTING_LABEL_LINESPACE - 1.),
+            expr::load_var("w"),
+            expr::const_f32(SETTING_LABEL_LINESPACE),
+            [0.15, 0.2, 0.19, 1.],
+        );
+
+        let node =
+            node.setup(|me| VectorArt::new(me, shape, app.render_api.clone(), app.ex.clone())).await;
+        layer_node.clone().link(node);
+
+        // Border right arrow
+        let node = create_vector_art("right_arrow");
+        let prop = node.get_property("rect").unwrap();
+        prop.clone().set_expr(atom, Role::App, 0, cc.compile("w * X_RATIO - BORDER_RIGHT_SCALE/2").unwrap()).unwrap();
+        prop.clone().set_f32(atom, Role::App, 1, setting_y+30.).unwrap();
+        prop.clone().set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.clone().set_f32(atom, Role::App, 3, 0.).unwrap();
+        node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
+        let shape = shape::create_right_border([0., 0.11, 0.11, 1.]).scaled(30.);
+        let node =
+            node.setup(|me| VectorArt::new(me, shape, app.render_api.clone(), app.ex.clone())).await;
+        layer_node.clone().link(node);
+
+        // Background Value
+        let node = create_vector_art(&(setting_name.to_string() + "_settings_value_bg"));
+        let prop = node.get_property("rect").unwrap();
+        prop.clone().set_expr(atom, Role::App, 0, cc.compile("w * X_RATIO - BORDER_RIGHT_SCALE/2").unwrap()).unwrap();
+        prop.clone().set_f32(atom, Role::App, 1, setting_y).unwrap();
+        prop.clone().set_expr(atom, Role::App, 2, cc.compile("w * (1-X_RATIO) + BORDER_RIGHT_SCALE/2").unwrap()).unwrap();
+        prop.clone().set_f32(atom, Role::App, 3, SETTING_LABEL_LINESPACE).unwrap();
+        node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
+        node.set_property_bool(atom, Role::App, "is_visible", false).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 = expr::const_f32(SETTING_LABEL_LINESPACE);
+        let (color1, color2) = match COLOR_SCHEME {
+            ColorScheme::DarkMode => ([0., 0.11, 0.11, 1.], [0., 0.11, 0.11, 1.]),
+            ColorScheme::PaperLight => ([1., 1., 1., 1.], [1., 1., 1., 1.]),
+        };
+        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);
+
+        shape.add_filled_box(
+            expr::const_f32(0.),
+            expr::const_f32(SETTING_LABEL_LINESPACE - 1.),
+            expr::load_var("w"),
+            expr::const_f32(SETTING_LABEL_LINESPACE),
+            [0.15, 0.2, 0.19, 1.],
+        );
+
+        let node =
+            node.setup(|me| VectorArt::new(me, shape, app.render_api.clone(), app.ex.clone())).await;
+        layer_node.clone().link(node);
+
+        // Label: "SETTINGS" title
+        let label_value_node = create_text(&(setting_name.to_string() + "_settings_label"));
+        let prop = label_value_node.get_property("rect").unwrap();
+        prop.clone().set_f32(atom, Role::App, 0, SETTING_LABEL_X).unwrap();
+        prop.clone().set_f32(atom, Role::App, 1, setting_y).unwrap();
+        prop.clone().set_expr(atom, Role::App, 2, cc.compile("w * X_RATIO").unwrap()).unwrap();
+        prop.clone().set_f32(atom, Role::App, 3, 100.).unwrap();
+        label_value_node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
+        label_value_node.set_property_f32(atom, Role::App, "baseline", SETTING_LABEL_BASELINE).unwrap();
+        label_value_node.set_property_f32(atom, Role::App, "font_size", SETTING_LABEL_FONTSIZE).unwrap();
+        label_value_node.set_property_str(atom, Role::App, "text", setting_name.clone()).unwrap();
+        let prop = label_value_node.get_property("text_color").unwrap();
+        if COLOR_SCHEME == ColorScheme::DarkMode {
+            prop.clone().set_f32(atom, Role::App, 0, 0.65).unwrap();
+            prop.clone().set_f32(atom, Role::App, 1, 0.87).unwrap();
+            prop.clone().set_f32(atom, Role::App, 2, 0.83).unwrap();
+            prop.clone().set_f32(atom, Role::App, 3, 1.).unwrap();
+        } else if COLOR_SCHEME == ColorScheme::PaperLight {
+            prop.clone().set_f32(atom, Role::App, 0, 0.).unwrap();
+            prop.clone().set_f32(atom, Role::App, 1, 0.).unwrap();
+            prop.clone().set_f32(atom, Role::App, 2, 0.).unwrap();
+            prop.clone().set_f32(atom, Role::App, 3, 1.).unwrap();
+        }
+        label_value_node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
+
+        let label_value_node = label_value_node
+            .setup(|me| {
+                Text::new(
+                    me,
+                    window_scale.clone(),
+                    app.render_api.clone(),
+                    app.text_shaper.clone(),
+                    app.ex.clone(),
+                )
+            })
+            .await;
+        layer_node.clone().link(label_value_node);
+
+        // Text edit
+        let editbox_node = create_editbox(&(setting_name.to_string() + "_editz"));
+        editbox_node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+        editbox_node.set_property_bool(atom, Role::App, "is_focused", true).unwrap();
+        let prop = editbox_node.get_property("rect").unwrap();
+        prop.clone().set_expr(atom, Role::App, 0, cc.compile("w * X_RATIO + BORDER_RIGHT_SCALE").unwrap()).unwrap();
+        prop.clone().set_f32(atom, Role::App, 1, setting_y + 15. ).unwrap();
+        prop.clone().set_expr(atom, Role::App, 2, cc.compile("w * X_RATIO - BORDER_RIGHT_SCALE").unwrap()).unwrap();
+        prop.clone().set_f32(atom, Role::App, 3, SETTING_LABEL_LINESPACE).unwrap();
+        editbox_node.set_property_f32(atom, Role::App, "baseline", 16.).unwrap();
+        editbox_node.set_property_f32(atom, Role::App, "font_size", SETTING_EDIT_FONTSIZE).unwrap();
+        let prop = editbox_node.get_property("text_color").unwrap();
+        if COLOR_SCHEME == ColorScheme::DarkMode {
+            prop.clone().set_f32(atom, Role::App, 0, 0.7).unwrap();
+            prop.clone().set_f32(atom, Role::App, 1, 0.7).unwrap();
+            prop.clone().set_f32(atom, Role::App, 2, 0.7).unwrap();
+            prop.clone().set_f32(atom, Role::App, 3, 1.).unwrap();
+        } else if COLOR_SCHEME == ColorScheme::PaperLight {
+            prop.clone().set_f32(atom, Role::App, 0, 1.).unwrap();
+            prop.clone().set_f32(atom, Role::App, 1, 1.).unwrap();
+            prop.clone().set_f32(atom, Role::App, 2, 1.).unwrap();
+            prop.clone().set_f32(atom, Role::App, 3, 1.).unwrap();
+        }
+        let prop = editbox_node.get_property("cursor_color").unwrap();
+        prop.clone().set_f32(atom, Role::App, 0, 0.5).unwrap();
+        prop.clone().set_f32(atom, Role::App, 1, 0.5).unwrap();
+        prop.clone().set_f32(atom, Role::App, 2, 0.5).unwrap();
+        prop.clone().set_f32(atom, Role::App, 3, 1.).unwrap();
+        editbox_node.set_property_f32(atom, Role::App, "cursor_ascent", SETTING_EDIT_FONTSIZE).unwrap();
+        editbox_node.set_property_f32(atom, Role::App, "cursor_descent", SETTING_EDIT_FONTSIZE).unwrap();
+        editbox_node.set_property_f32(atom, Role::App, "select_ascent", SETTING_EDIT_FONTSIZE).unwrap();
+        editbox_node.set_property_f32(atom, Role::App, "select_descent", SETTING_EDIT_FONTSIZE).unwrap();
+        let prop = editbox_node.get_property("hi_bg_color").unwrap();
+        if COLOR_SCHEME == ColorScheme::DarkMode {
+            prop.clone().set_f32(atom, Role::App, 0, 0.5).unwrap();
+            prop.clone().set_f32(atom, Role::App, 1, 0.5).unwrap();
+            prop.clone().set_f32(atom, Role::App, 2, 0.5).unwrap();
+            prop.clone().set_f32(atom, Role::App, 3, 1.).unwrap();
+        } else if COLOR_SCHEME == ColorScheme::PaperLight {
+            prop.clone().set_f32(atom, Role::App, 0, 1.).unwrap();
+            prop.clone().set_f32(atom, Role::App, 1, 1.).unwrap();
+            prop.clone().set_f32(atom, Role::App, 2, 1.).unwrap();
+            prop.clone().set_f32(atom, Role::App, 3, 0.5).unwrap();
+        }
+        let prop = editbox_node.get_property("selected").unwrap();
+        prop.clone().set_null(atom, Role::App, 0).unwrap();
+        prop.clone().set_null(atom, Role::App, 1).unwrap();
+        editbox_node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
+        editbox_node.set_property_bool(atom, Role::App, "is_active", false).unwrap();
+        editbox_node.set_property_bool(atom, Role::App, "is_focused", false).unwrap();
+        editbox_node.set_property_f32(atom, Role::App, "font_size", 0.).unwrap();
+
+        let editz_text = PropertyStr::wrap(&editbox_node, Role::App, "text", 0).unwrap();
+
+        // Handle enter pressed in the editbox
+        {
+            let (slot, recvr) = Slot::new("setting_enter_pressed");
+            editbox_node.register("enter_pressed", slot).unwrap();
+            let setting2 = setting.clone();
+            let sg_root2 = app.sg_root.clone();
+            let active_setting2 = active_setting.clone();
+            let editz_text2 = editz_text.clone();
+            let listen_enter = app.ex.spawn(async move {
+                while let Ok(_) = recvr.recv().await {
+                    update_setting(setting2.clone(), sg_root2.clone(), active_setting2.clone(), editz_text2.clone()).await;
+                }
+            });
+            app.tasks.lock().unwrap().push(listen_enter);
+        }
+
+        let node = editbox_node
+            .setup(|me| {
+                EditBox::new(
+                    me,
+                    window_scale.clone(),
+                    app.render_api.clone(),
+                    app.text_shaper.clone(),
+                    app.ex.clone(),
+                )
+            })
+            .await;
+        layer_node.clone().link(node);
+
+        // Is this setting the one that is currently active
+        let cloned_active_setting = active_setting.clone();
+        let is_active_setting = match active_setting.lock().unwrap().as_ref() {
+            Some(active) => Arc::ptr_eq(active, &setting),
+            None => false,
+        };
+        let is_bool = matches!(setting_clone.get_value(), PropertyValue::Bool(_));
+
+        if !is_active_setting {
+            // Label showing the setting's current value
+            let value_node = create_text(&(setting_name.to_string() + "_settings_value_label"));
+            let prop = value_node.get_property("rect").unwrap();
+            prop.clone().set_expr(atom, Role::App, 0, cc.compile("w * X_RATIO + BORDER_RIGHT_SCALE").unwrap()).unwrap();
+            prop.clone().set_f32(atom, Role::App, 1, setting_y).unwrap();
+            prop.clone().set_expr(atom, Role::App, 2, cc.compile("w * X_RATIO").unwrap()).unwrap();
+            prop.clone().set_f32(atom, Role::App, 3, 100.).unwrap();
+            value_node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
+            value_node.set_property_f32(atom, Role::App, "baseline", SETTING_LABEL_BASELINE).unwrap();
+            value_node.set_property_f32(atom, Role::App, "font_size", SETTING_LABEL_FONTSIZE).unwrap();
+            value_node.set_property_str(atom, Role::App, "text", setting_clone.value_as_string()).unwrap();
+            let prop = value_node.get_property("text_color").unwrap();
+            if COLOR_SCHEME == ColorScheme::DarkMode {
+                prop.clone().set_f32(atom, Role::App, 0, 1.).unwrap();
+                prop.clone().set_f32(atom, Role::App, 1, 1.).unwrap();
+                prop.clone().set_f32(atom, Role::App, 2, 1.).unwrap();
+                prop.clone().set_f32(atom, Role::App, 3, 1.).unwrap();
+            } else if COLOR_SCHEME == ColorScheme::PaperLight {
+                prop.clone().set_f32(atom, Role::App, 0, 0.).unwrap();
+                prop.clone().set_f32(atom, Role::App, 1, 0.).unwrap();
+                prop.clone().set_f32(atom, Role::App, 2, 0.).unwrap();
+                prop.clone().set_f32(atom, Role::App, 3, 1.).unwrap();
+            }
+            value_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+
+            let node = value_node
+                .setup(|me| {
+                    Text::new(
+                        me,
+                        window_scale.clone(),
+                        app.render_api.clone(),
+                        app.text_shaper.clone(),
+                        app.ex.clone(),
+                    )
+                })
+                .await;
+            layer_node.clone().link(node);
+
+            // A wide button useful to select the current setting
+            let node = create_button("setting_selector_btn");
+            node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+            let prop = node.get_property("rect").unwrap();
+            prop.clone().set_expr(atom, Role::App, 0, cc.compile("w * X_RATIO").unwrap()).unwrap();
+            prop.clone().set_f32(atom, Role::App, 1, setting_y).unwrap();
+            prop.clone().set_expr(atom, Role::App, 2, cc.compile("w * (1-X_RATIO)").unwrap()).unwrap();
+            prop.clone().set_f32(atom, Role::App, 3, SETTING_LABEL_LINESPACE).unwrap();
+
+            let sg_root = app.sg_root.clone();
+            let select = move || {
+                let atom = &mut PropertyAtomicGuard::new();
+                let sg_root = sg_root.clone();
+                let mut lock = cloned_active_setting.lock().unwrap();
+
+                let was_active = if let Some(s) = lock.as_ref() {
+                    let _was_active = s.name == setting_clone.clone().name;
+
+                    // Hide the selected setting value label (set its text empty)
+                    // of the selected setting, if there's one
+                    let path = format!("/window/settings_layer/{}_settings_value_label", &s.name);
+                    let node = sg_root.clone().lookup_node(&path).unwrap();
+                    let text = PropertyStr::wrap(&node, Role::App, "text", 0).unwrap();
+                    text.set(atom, &s.value_as_string());
+
+                    // Hide the selected setting value label (set its text empty)
+                    // of the selected setting, if there's one
+                    if !_was_active {
+                        let path = format!("/window/settings_layer/{}_editz", &s.name);
+                        let node = sg_root.clone().lookup_node(&path).unwrap();
+                        node.set_property_bool(atom, Role::App, "is_active", false).unwrap();
+                        node.set_property_bool(atom, Role::App, "is_focused", false).unwrap();
+                        node.set_property_f32(atom, Role::App, "font_size", 0.).unwrap();
+                        node.set_property_str(atom, Role::App, "text", "").unwrap();
+                    }
+                    
+                    // Hide confirm button
+                    // (Bool settings don't have a confirm button so we have to check for it to
+                    // not panic)
+                    let is_bool = matches!(lock.clone().unwrap().get_value(), PropertyValue::Bool(_));
+                    if !is_bool {
+                        let path = format!("/window/settings_layer/{}_confirm_btn_bg", &s.name);
+                        let node = sg_root.clone().lookup_node(&path).unwrap();
+                        node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
+                    }
+
+                    _was_active
+                } else {
+                    false
+                };
+
+                // Update what setting active_setting points to 
+                *lock = Some(setting_clone.clone());
+                debug!("active setting set to: {}", lock.clone().unwrap().name);
+
+                let is_bool = matches!(lock.clone().unwrap().get_value(), PropertyValue::Bool(_));
+                if is_bool {
+                    // Hide the setting value label (set its text empty) 
+                    let label_path = format!("/window/settings_layer/{}_settings_value_label", &setting_clone.name);
+                    let editbox = sg_root.clone().lookup_node(&label_path).unwrap();
+                    let label_text = PropertyStr::wrap(&editbox, Role::App, "text", 0).unwrap();
+                    let value = &lock.clone().unwrap().get_value();
+                    if matches!(value, PropertyValue::Bool(false)) {
+                        &lock.clone().unwrap().node.set_property_bool(atom, Role::User, "value", true);
+                        label_text.set(atom, "true");
+                    } else {
+                        &lock.clone().unwrap().node.set_property_bool(atom, Role::User, "value", false);
+                        label_text.set(atom, "false");
+                    }
+                } else {
+                    // Hide the setting value label (set its text empty) 
+                    // TODO?: Visilibity property on labels
+                    let label_path = format!("/window/settings_layer/{}_settings_value_label", &setting_clone.name);
+                    let editbox = sg_root.clone().lookup_node(&label_path).unwrap();
+                    let label_text = PropertyStr::wrap(&editbox, Role::App, "text", 0).unwrap();
+                    label_text.set(atom, "");
+
+                    // Show the editbox
+                    let path = format!("/window/settings_layer/{}_editz", &setting_clone.name);
+                    let node = sg_root.clone().lookup_node(&path).unwrap();
+                    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+                    node.set_property_bool(atom, Role::App, "is_focused", true).unwrap();
+                    node.set_property_f32(atom, Role::App, "font_size", 16.).unwrap();
+                    if !was_active {
+                        node.set_property_str(atom, Role::App, "text", setting_clone.value_as_string()).unwrap();
+                    }
+
+                    // Show confirm button
+                    let path = format!("/window/settings_layer/{}_confirm_btn_bg", &setting_clone.name);
+                    let node = sg_root.clone().lookup_node(&path).unwrap();
+                    let _ = node.set_property_bool(atom, Role::App, "is_visible", true);
+
+                    // Hide the value background
+                    let path = format!("/window/settings_layer/{}_settings_value_bg", &setting_clone.name);
+                    let node = sg_root.clone().lookup_node(&path).unwrap();
+                    let _ = node.set_property_bool(atom, Role::App, "is_visible", false);
+                }
+            };
+
+            let (slot, recvr) = Slot::new("back_clicked");
+            node.register("click", slot).unwrap();
+            let select2 = select.clone();
+            let listen_click = app.ex.spawn(async move {
+                while let Ok(_) = recvr.recv().await {
+                    select2();
+                }
+            });
+            app.tasks.lock().unwrap().push(listen_click);
+
+            let node = node.setup(|me| Button::new(me, app.ex.clone())).await;
+            layer_node.clone().link(node.clone());
+        }
+
+        if is_bool {
+            // Switch icon
+            let node = create_vector_art(&(setting_name.to_string() + "_switch_btn_bg"));
+            let prop = node.get_property("rect").unwrap();
+            prop.clone().set_expr(atom, Role::App, 0, cc.compile("w - 50").unwrap()).unwrap();
+            prop.clone().set_f32(atom, Role::App, 1, setting_y + SETTING_LABEL_LINESPACE / 2.).unwrap();
+            prop.clone().set_f32(atom, Role::App, 2, 0.).unwrap();
+            prop.clone().set_f32(atom, Role::App, 3, 0.).unwrap();
+            node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
+
+            let shape = shape::create_switch([0., 0.94, 1., 1.]).scaled(10.);
+            let node =
+                node.setup(|me| VectorArt::new(me, shape, app.render_api.clone(), app.ex.clone())).await;
+            layer_node.clone().link(node);
+        } else {
+            // Confirm button
+            let node = create_vector_art(&(setting_name.to_string() + "_confirm_btn_bg"));
+            let prop = node.get_property("rect").unwrap();
+            prop.clone().set_expr(atom, Role::App, 0, cc.compile("w - 50").unwrap()).unwrap();
+            prop.clone().set_f32(atom, Role::App, 1, setting_y + SETTING_LABEL_LINESPACE / 2.).unwrap();
+            prop.clone().set_f32(atom, Role::App, 2, 0.).unwrap();
+            prop.clone().set_f32(atom, Role::App, 3, 0.).unwrap();
+            node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
+            node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
+
+            let shape = shape::create_confirm([0., 0.94, 1., 1.]).scaled(10.);
+            let node =
+                node.setup(|me| VectorArt::new(me, shape, app.render_api.clone(), app.ex.clone())).await;
+            layer_node.clone().link(node.clone());
+
+            let node = create_button("confirm_btn");
+            node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+            let prop = node.get_property("rect").unwrap();
+            prop.clone().set_expr(atom, Role::App, 0, cc.compile("w - 100").unwrap()).unwrap();
+            prop.clone().set_f32(atom, Role::App, 1, setting_y).unwrap();
+            prop.clone().set_f32(atom, Role::App, 2, 100.).unwrap();
+            prop.clone().set_f32(atom, Role::App, 3, SETTING_LABEL_LINESPACE).unwrap();
+            node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
+
+            let node = node.setup(|me| Button::new(me, app.ex.clone())).await;
+            layer_node.clone().link(node.clone());
+
+            // Handle confirm button click
+            {
+                let (slot, recvr) = Slot::new("confirm_clicked");
+                node.register("click", slot).unwrap();
+                let setting2 = setting.clone();
+                let sg_root2 = app.sg_root.clone();
+                let active_setting2 = active_setting.clone();
+                let editz_text2 = editz_text.clone();
+                let listen_click = app.ex.spawn(async move {
+                    while let Ok(_) = recvr.recv().await {
+                        info!("confirm clicked");
+                        update_setting(setting2.clone(), sg_root2.clone(), active_setting2.clone(), editz_text2.clone()).await;
+                    }
+                });
+                app.tasks.lock().unwrap().push(listen_click);
+            }
+        }
+    }
+
+    let settings_node = app.sg_root.clone().lookup_node("/window/settings_layer").unwrap();
+    settings_node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
+}
+
+async fn update_setting(setting: Arc<Setting>, sg_root: SceneNodePtr, active_setting: Arc<Mutex<Option<Arc<Setting>>>>, editz_text: PropertyStr) {
+    let atom = &mut PropertyAtomicGuard::new();
+    let setting_name = &setting.clone().name;
+
+    let path = format!("/window/settings_layer/{}_editz", setting_name);
+    if let Some(node) = sg_root.clone().lookup_node(path) {
+        node.set_property_f32(atom, Role::App, "font_size", 0.).unwrap();
+        node.set_property_bool(atom, Role::App, "is_active", false).unwrap();
+        node.set_property_bool(atom, Role::App, "is_focused", false).unwrap();
+    }
+
+    match &setting.get_value() {
+        PropertyValue::Uint32(_) => {
+            let value_str = editz_text.get();
+            let parsed = value_str.parse::<u32>();
+            if let Ok(value) = parsed {
+                let path = format!("/window/settings_layer/{}_settings_value_label", setting_name);
+                if let Some(node) = sg_root.clone().lookup_node(path) {
+                    node.set_property_str(atom, Role::App, "text", value_str).unwrap();
+                }
+                let path = format!("/window/settings_layer/{}_confirm_btn_bg", setting_name);
+                if let Some(node) = sg_root.clone().lookup_node(path) {
+                    node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
+                }
+                setting.node.set_property_u32(atom, Role::User, "value", value);
+                let mut active_setting_value = active_setting.lock().unwrap();
+                *active_setting_value = None;
+            }
+        },
+        PropertyValue::Float32(_) => {
+            let value_str = editz_text.get();
+            let parsed = value_str.parse::<f32>();
+            if let Ok(value) = parsed {
+                let path = format!("/window/settings_layer/{}_settings_value_label", setting_name);
+                if let Some(node) = sg_root.clone().lookup_node(path) {
+                    node.set_property_str(atom, Role::App, "text", value_str).unwrap();
+                }
+                let path = format!("/window/settings_layer/{}_confirm_btn_bg", setting_name);
+                if let Some(node) = sg_root.clone().lookup_node(path) {
+                    node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
+                }
+                setting.node.set_property_f32(atom, Role::User, "value", value);
+                let mut active_setting_value = active_setting.lock().unwrap();
+                *active_setting_value = None;
+            }
+        },
+        PropertyValue::Str(_) => {
+            let value_str = editz_text.get();
+            let path = format!("/window/settings_layer/{}_settings_value_label", setting_name);
+            if let Some(node) = sg_root.clone().lookup_node(path) {
+                node.set_property_str(atom, Role::App, "text", &value_str).unwrap();
+            }
+            let path = format!("/window/settings_layer/{}_confirm_btn_bg", setting_name);
+            if let Some(node) = sg_root.clone().lookup_node(path) {
+                node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
+            }
+            setting.node.set_property_str(atom, Role::User, "value", value_str);
+            let mut active_setting_value = active_setting.lock().unwrap();
+            *active_setting_value = None;
+        },
+        _ => {},
+    };
+}

+ 45 - 5
bin/app/src/plugin/darkirc.rs

@@ -40,8 +40,8 @@ use std::{
 
 use crate::{
     error::{Error, Result},
-    prop::{PropertyAtomicGuard, PropertyStr, Role},
-    scene::{MethodCallSub, Pimpl, SceneNodePtr, SceneNodeWeak},
+    prop::{PropertyAtomicGuard, PropertyStr, PropertyPtr, PropertyType, PropertyValue, Role},
+    scene::{MethodCallSub, Pimpl, SceneNode, SceneNodeType, SceneNodePtr, SceneNodeWeak},
     ui::{
         chatview::{MessageId, Timestamp},
         OnModify,
@@ -49,7 +49,7 @@ use crate::{
     ExecutorPtr,
 };
 
-use super::PluginObject;
+use super::{PluginObject, PluginSettings};
 
 const P2P_RETRY_TIME: u64 = 20;
 const COOLOFF_SLEEP_TIME: u64 = 20;
@@ -175,6 +175,8 @@ pub struct DarkIrc {
 
     seen_msgs: SyncMutex<SeenMessages>,
     nick: PropertyStr,
+
+    settings: PluginSettings,
 }
 
 impl DarkIrc {
@@ -182,6 +184,9 @@ impl DarkIrc {
         let node_ref = &node.upgrade().unwrap();
         let nick = PropertyStr::wrap(node_ref, Role::Internal, "nick", 0).unwrap();
 
+        let mut setting_root = Arc::new(SceneNode::new("setting", SceneNodeType::SettingRoot));
+        node_ref.clone().link(setting_root.clone());
+
         i!("Starting DarkIRC backend");
         let evgr_path = get_evgrdb_path();
         let db = match sled::open(&evgr_path) {
@@ -192,6 +197,12 @@ impl DarkIrc {
             }
         };
 
+        let setting_tree = db.open_tree("settings")?;
+        let settings = PluginSettings {
+            setting_root,
+            sled_tree: setting_tree,
+        };
+
         let mut p2p_settings: NetSettings = Default::default();
         p2p_settings.app_version = semver::Version::parse("0.5.0").unwrap();
         if get_use_tor_filename().exists() {
@@ -225,7 +236,25 @@ impl DarkIrc {
         p2p_settings.p2p_datastore = p2p_datastore_path().into_os_string().into_string().ok();
         p2p_settings.hostlist = hostlist_path().into_os_string().into_string().ok();
 
-        let p2p = match P2p::new(p2p_settings, ex.clone()).await {
+        let node_outbound_connect_timeout = settings.add_setting("net.outbound_connect_timeout", PropertyValue::Uint32(p2p_settings.outbound_connect_timeout as u32)).unwrap();
+        let node_channel_handshake_timeout = settings.add_setting("net.channel_handshake_timeout", PropertyValue::Uint32(p2p_settings.channel_handshake_timeout as u32)).unwrap();
+        let node_channel_heartbeat_interval = settings.add_setting("net.channel_heartbeat_interval", PropertyValue::Uint32(p2p_settings.channel_heartbeat_interval as u32)).unwrap();
+        let node_outbound_peer_discovery_cooloff_time = settings.add_setting("net.outbound_peer_discovery_cooloff_time", PropertyValue::Uint32(p2p_settings.outbound_peer_discovery_cooloff_time as u32)).unwrap();
+        let node_slot_preference_strict = settings.add_setting("net.slot_preference_strict", PropertyValue::Bool(p2p_settings.slot_preference_strict)).unwrap();
+        let node_transport_mixing = settings.add_setting("net.transport_mixing", PropertyValue::Bool(p2p_settings.transport_mixing)).unwrap();
+        let node_localnet = settings.add_setting("net.localnet", PropertyValue::Bool(p2p_settings.localnet)).unwrap();
+
+        settings.load_settings();
+
+        p2p_settings.outbound_connect_timeout = node_outbound_connect_timeout.get_property_u32("value").unwrap() as u64;
+        p2p_settings.channel_handshake_timeout = node_channel_handshake_timeout.get_property_u32("value").unwrap() as u64;
+        p2p_settings.channel_heartbeat_interval = node_channel_heartbeat_interval.get_property_u32("value").unwrap() as u64;
+        p2p_settings.outbound_peer_discovery_cooloff_time = node_outbound_peer_discovery_cooloff_time.get_property_u32("value").unwrap() as u64;
+        p2p_settings.slot_preference_strict = node_slot_preference_strict.get_property_bool("value").unwrap();
+        p2p_settings.transport_mixing = node_transport_mixing.get_property_bool("value").unwrap();
+        p2p_settings.localnet = node_localnet.get_property_bool("value").unwrap();
+
+        let p2p = match P2p::new(p2p_settings.clone(), ex.clone()).await {
             Ok(p2p) => p2p,
             Err(err) => {
                 e!("Create p2p network failed: {err}!");
@@ -256,7 +285,7 @@ impl DarkIrc {
         }
 
         let self_ = Arc::new(Self {
-            node,
+            node: node.clone(),
             tasks: OnceLock::new(),
 
             p2p,
@@ -265,6 +294,7 @@ impl DarkIrc {
 
             seen_msgs: SyncMutex::new(SeenMessages::new()),
             nick,
+            settings,
         });
         Ok(Pimpl::DarkIrc(self_))
     }
@@ -479,6 +509,11 @@ impl DarkIrc {
 
         self.p2p.broadcast(&EventPut(event)).await;
     }
+
+    async fn apply_settings(self_: Arc<Self>) {
+        self_.settings.save_settings();
+        i!("TODO: Apply darkirc settings");
+    }
 }
 
 #[async_trait]
@@ -509,6 +544,11 @@ impl PluginObject for DarkIrc {
         }
         on_modify.when_change(self.nick.prop(), save_nick);
 
+        // `apply_settings` is triggered if any setting changes
+        for setting_node in self.settings.setting_root.get_children().iter() {
+            on_modify.when_change(setting_node.get_property("value").clone().unwrap(), Self::apply_settings);
+        }
+
         let ev_sub = self.event_graph.event_pub.clone().subscribe().await;
         let ev_task = ex.spawn(self.clone().relay_events(ev_sub));
 

+ 149 - 0
bin/app/src/plugin/mod.rs

@@ -24,7 +24,156 @@ use crate::ExecutorPtr;
 pub mod darkirc;
 pub use darkirc::{DarkIrc, DarkIrcPtr};
 
+use sled_overlay::sled;
+use crate::{
+    scene::{MethodCallSub, Pimpl, SceneNode, SceneNodeType, SceneNodePtr, SceneNodeWeak},
+    prop::{Property, PropertyAtomicGuard, PropertyStr, PropertyPtr, PropertyType, PropertySubType, PropertyValue, Role},
+};
+use std::array::TryFromSliceError;
+use std::string::FromUtf8Error;
+
 #[async_trait]
 pub trait PluginObject {
     async fn start(self: Arc<Self>, ex: ExecutorPtr) {}
 }
+
+pub struct PluginSettings {
+    setting_root: SceneNodePtr,
+    sled_tree: sled::Tree,
+}
+impl PluginSettings {
+    fn add_setting(&self, name: &str, default: PropertyValue) -> Option<SceneNodePtr> {
+        let atom = &mut PropertyAtomicGuard::new();
+        let node = match default {
+            PropertyValue::Bool(b) => {
+                let mut node = SceneNode::new(name, SceneNodeType::Setting);
+                let prop = Property::new("value", PropertyType::Bool, PropertySubType::Null);
+                node.add_property(prop).unwrap();
+                let prop = Property::new("default", PropertyType::Bool, PropertySubType::Null);
+                node.add_property(prop).unwrap();
+                node.set_property_bool(atom, Role::User, "value", b.clone());
+                node.set_property_bool(atom, Role::App, "default", b.clone());
+                Some(node)
+            }
+            PropertyValue::Uint32(u) => {
+                let mut node = SceneNode::new(name, SceneNodeType::Setting);
+                let prop = Property::new("value", PropertyType::Uint32, PropertySubType::Null);
+                node.add_property(prop).unwrap();
+                let prop = Property::new("default", PropertyType::Uint32, PropertySubType::Null);
+                node.add_property(prop).unwrap();
+                node.set_property_u32(atom, Role::User, "value", u.clone());
+                node.set_property_u32(atom, Role::App, "default", u.clone());
+                Some(node)
+            }
+            PropertyValue::Float32(f) => {
+                let mut node = SceneNode::new(name, SceneNodeType::Setting);
+                let prop = Property::new("value", PropertyType::Float32, PropertySubType::Null);
+                node.add_property(prop).unwrap();
+                let prop = Property::new("default", PropertyType::Float32, PropertySubType::Null);
+                node.add_property(prop).unwrap();
+                node.set_property_f32(atom, Role::User, "value", f.clone());
+                node.set_property_f32(atom, Role::App, "default", f.clone());
+                Some(node)
+            }
+            PropertyValue::Str(s) => {
+                let mut node = SceneNode::new(name, SceneNodeType::Setting);
+                let prop = Property::new("value", PropertyType::Str, PropertySubType::Null);
+                node.add_property(prop).unwrap();
+                let prop = Property::new("default", PropertyType::Str, PropertySubType::Null);
+                node.add_property(prop).unwrap();
+                node.set_property_str(atom, Role::User, "value", s.clone());
+                node.set_property_str(atom, Role::App, "default", s.clone());
+                Some(node)
+            }
+            _ => { None }
+        };
+
+        match node {
+            Some(n) => {
+                let node_ptr = Arc::new(n);
+                self.setting_root.clone().link(node_ptr.clone().into());
+                Some(node_ptr)
+            }
+            None => None
+        }
+    }
+
+    // For all settings, copy the value from sled into the setting node's value property
+    fn load_settings(&self) {
+        let atom = &mut PropertyAtomicGuard::new();
+        for setting_node in self.setting_root.get_children().iter() {
+            if setting_node.typ != SceneNodeType::Setting {
+                continue;
+            }
+
+            let value = setting_node.get_property("value").clone().unwrap();
+            match value.typ {
+                PropertyType::Bool => {
+                    let sled_result = self.sled_tree.get(setting_node.name.as_str());
+                    if let Ok(Some(sled_value)) = sled_result {
+                        setting_node.set_property_bool(atom, Role::User, "value", sled_value[0] != 0);
+                    }
+                }
+                PropertyType::Uint32 => {
+                    let sled_result = self.sled_tree.get(setting_node.name.as_str());
+                    if let Ok(Some(sled_value)) = sled_result {
+                        if sled_value.len() == 4 {
+                            let bytes: Result<[u8; 4], TryFromSliceError> = sled_value.as_ref().try_into();
+                            if let Ok(b) = bytes {
+                                setting_node.set_property_u32(atom, Role::User, "value", u32::from_le_bytes(b));
+                            }
+                        }
+                    }
+                }
+                PropertyType::Float32 => {
+                    let sled_result = self.sled_tree.get(setting_node.name.as_str());
+                    if let Ok(Some(sled_value)) = sled_result {
+                        if sled_value.len() == 4 {
+                            let bytes: Result<[u8; 4], TryFromSliceError> = sled_value.as_ref().try_into();
+                            if let Ok(b) = bytes {
+                                setting_node.set_property_f32(atom, Role::User, "value", f32::from_le_bytes(b));
+                            }
+                        }
+                    }
+                }
+                PropertyType::Str => {
+                    let sled_result = self.sled_tree.get(setting_node.name.as_str());
+                    if let Ok(Some(sled_value)) = sled_result {
+                        let string: Result<String, FromUtf8Error> = String::from_utf8(sled_value.to_vec());
+                        if let Ok(s) = string {
+                            setting_node.set_property_str(atom, Role::User, "value", s);
+                        }
+                    }
+                }
+                _ => {}
+            }
+        }
+    }
+
+    // Save all settings to sled
+    fn save_settings(&self) {
+        for setting_node in self.setting_root.get_children().iter() {
+            if setting_node.typ != SceneNodeType::Setting {
+                continue;
+            }
+
+            let value = setting_node.get_property("value").clone().unwrap();
+            match value.typ {
+                PropertyType::Bool => {
+                    let value_bytes = if value.get_bool(0).unwrap() { 1u8 } else { 0u8 };
+                    self.sled_tree.insert(setting_node.name.as_str(), sled::IVec::from(vec![value_bytes]));
+                }
+                PropertyType::Uint32 => {
+                    self.sled_tree.insert(setting_node.name.as_str(), sled::IVec::from(value.get_u32(0).unwrap().to_le_bytes().as_ref()));
+                }
+                PropertyType::Float32 => {
+                    self.sled_tree.insert(setting_node.name.as_str(), sled::IVec::from(value.get_f32(0).unwrap().to_le_bytes().as_ref()));
+                }
+                PropertyType::Str => {
+                    self.sled_tree.insert(setting_node.name.as_str(), sled::IVec::from(value.get_str(0).unwrap().as_bytes()));
+                }
+                _ => {}
+            }
+        }
+    }
+}

+ 2 - 0
bin/app/src/scene.rs

@@ -119,6 +119,8 @@ pub enum SceneNodeType {
     Shortcut = 21,
     Gesture = 22,
     EmojiPicker = 23,
+    SettingRoot = 24,
+    Setting = 25,
     PluginRoot = 100,
     Plugin = 101,
 }

+ 17 - 0
bin/app/src/shape/confirm.rs

@@ -0,0 +1,17 @@
+use crate::{mesh::Color, ui::{VectorShape, ShapeVertex}};
+pub fn create_confirm(color: Color) -> VectorShape {
+    VectorShape {
+        verts: vec![
+            ShapeVertex::from_xy(-0.52, 0.6, color),
+            ShapeVertex::from_xy(-0.52, -0.6, color),
+            ShapeVertex::from_xy(0.5, 0.0, color),
+            ShapeVertex::from_xy(-1.05, 1.5, color),
+            ShapeVertex::from_xy(-1.05, -1.5, color),
+            ShapeVertex::from_xy(1.5, 0.0, color),
+            ShapeVertex::from_xy(-0.88, 1.2, color),
+            ShapeVertex::from_xy(-0.88, -1.2, color),
+            ShapeVertex::from_xy(1.16, 0.0, color),
+        ],
+        indices: vec![0, 2, 1, 5, 6, 3, 5, 7, 8, 3, 7, 4, 5, 8, 6, 5, 4, 7, 3, 6, 7]
+    }
+}

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

@@ -34,3 +34,13 @@ mod netlogo2;
 pub use netlogo2::create_netlogo2;
 mod netlogo3;
 pub use netlogo3::create_netlogo3;
+
+mod settings;
+pub use settings::create_settings;
+pub use settings::create_right_border;
+
+mod switch;
+pub use switch::create_switch;
+
+mod confirm;
+pub use confirm::create_confirm;

+ 88 - 0
bin/app/src/shape/settings.rs

@@ -0,0 +1,88 @@
+use crate::{mesh::Color, ui::{VectorShape, ShapeVertex}};
+pub fn create_settings(color: Color) -> VectorShape {
+    VectorShape {
+        verts: vec![
+            ShapeVertex::from_xy(-0.282843, 0.0, color),
+            ShapeVertex::from_xy(0.0, 0.282843, color),
+            ShapeVertex::from_xy(-0.0, -0.282843, color),
+            ShapeVertex::from_xy(0.282843, -0.0, color),
+            ShapeVertex::from_xy(-0.05, 1.0, color),
+            ShapeVertex::from_xy(0.05, 0.9, color),
+            ShapeVertex::from_xy(-0.05, -0.9, color),
+            ShapeVertex::from_xy(0.05, -1.0, color),
+            ShapeVertex::from_xy(-0.05, -0.335, color),
+            ShapeVertex::from_xy(0.05, -0.335, color),
+            ShapeVertex::from_xy(0.0, 0.95, color),
+            ShapeVertex::from_xy(-0.0, -0.95, color),
+            ShapeVertex::from_xy(-0.0, -0.385, color),
+            ShapeVertex::from_xy(-0.05, 0.3315, color),
+            ShapeVertex::from_xy(0.05, 0.3315, color),
+            ShapeVertex::from_xy(0.0, 0.3815, color),
+            ShapeVertex::from_xy(-0.982843, 0.45, color),
+            ShapeVertex::from_xy(-0.7, 0.732843, color),
+            ShapeVertex::from_xy(-0.7, 0.167157, color),
+            ShapeVertex::from_xy(-0.417157, 0.45, color),
+            ShapeVertex::from_xy(-0.75, 1.0, color),
+            ShapeVertex::from_xy(-0.65, 0.9, color),
+            ShapeVertex::from_xy(-0.75, -0.9, color),
+            ShapeVertex::from_xy(-0.65, -1.0, color),
+            ShapeVertex::from_xy(-0.75, 0.115, color),
+            ShapeVertex::from_xy(-0.65, 0.115, color),
+            ShapeVertex::from_xy(-0.7, 0.95, color),
+            ShapeVertex::from_xy(-0.7, -0.95, color),
+            ShapeVertex::from_xy(-0.7, 0.065, color),
+            ShapeVertex::from_xy(-0.75, 0.7815, color),
+            ShapeVertex::from_xy(-0.65, 0.7815, color),
+            ShapeVertex::from_xy(-0.7, 0.8315, color),
+            ShapeVertex::from_xy(0.417157, -0.45, color),
+            ShapeVertex::from_xy(0.7, -0.167157, color),
+            ShapeVertex::from_xy(0.7, -0.732843, color),
+            ShapeVertex::from_xy(0.982843, -0.45, color),
+            ShapeVertex::from_xy(0.65, 1.0, color),
+            ShapeVertex::from_xy(0.75, 0.9, color),
+            ShapeVertex::from_xy(0.65, -0.9, color),
+            ShapeVertex::from_xy(0.75, -1.0, color),
+            ShapeVertex::from_xy(0.65, -0.785, color),
+            ShapeVertex::from_xy(0.75, -0.785, color),
+            ShapeVertex::from_xy(0.7, 0.95, color),
+            ShapeVertex::from_xy(0.7, -0.95, color),
+            ShapeVertex::from_xy(0.7, -0.835, color),
+            ShapeVertex::from_xy(0.65, -0.1185, color),
+            ShapeVertex::from_xy(0.75, -0.1185, color),
+            ShapeVertex::from_xy(0.7, -0.0685, color),
+            ShapeVertex::from_xy(-0.869706, 0.45, color),
+            ShapeVertex::from_xy(-0.7, 0.619706, color),
+            ShapeVertex::from_xy(-0.7, 0.280294, color),
+            ShapeVertex::from_xy(-0.530294, 0.45, color),
+            ShapeVertex::from_xy(0.530294, -0.45, color),
+            ShapeVertex::from_xy(0.7, -0.280294, color),
+            ShapeVertex::from_xy(0.7, -0.619706, color),
+            ShapeVertex::from_xy(0.869706, -0.45, color),
+            ShapeVertex::from_xy(-0.169706, 0.0, color),
+            ShapeVertex::from_xy(0.0, 0.169706, color),
+            ShapeVertex::from_xy(-0.0, -0.169706, color),
+            ShapeVertex::from_xy(0.169706, -0.0, color),
+        ],
+        indices: vec![0, 58, 2, 9, 11, 12, 12, 6, 8, 10, 13, 4, 5, 15, 10, 18, 48, 50, 28, 23, 27, 28, 22, 24, 26, 29, 20, 21, 31, 26, 33, 52, 32, 41, 43, 44, 44, 38, 40, 36, 47, 45, 37, 47, 42, 3, 57, 1, 9, 7, 11, 12, 11, 6, 10, 15, 13, 5, 14, 15, 19, 49, 17, 28, 25, 23, 28, 27, 22, 26, 31, 29, 21, 30, 31, 34, 55, 35, 41, 39, 43, 44, 43, 38, 36, 42, 47, 37, 46, 47, 18, 51, 19, 17, 48, 16, 33, 55, 53, 32, 54, 34, 2, 59, 3, 1, 56, 0, 0, 56, 58, 18, 16, 48, 33, 53, 52, 3, 59, 57, 19, 51, 49, 34, 54, 55, 18, 50, 51, 17, 49, 48, 33, 35, 55, 32, 52, 54, 2, 58, 59, 1, 57, 56]
+    }
+}
+
+pub fn create_right_border(color: Color) -> VectorShape {
+    VectorShape {
+        verts: vec![
+            ShapeVertex::from_xy(-0.4, 1.0, color),
+            ShapeVertex::from_xy(0.5, 0.0, color),
+            ShapeVertex::from_xy(-0.4, -1.0, color),
+            ShapeVertex::from_xy(-0.5, 1.0, color),
+            ShapeVertex::from_xy(-0.5, 0.0, color),
+            ShapeVertex::from_xy(-0.5, -1.0, color),
+            ShapeVertex::from_xy(0.1, 1.0, color),
+            ShapeVertex::from_xy(1.0, 0.0, color),
+            ShapeVertex::from_xy(0.1, -1.0, color),
+            ShapeVertex::from_xy(-0.1, 1.0, color),
+            ShapeVertex::from_xy(0.8, 0.0, color),
+            ShapeVertex::from_xy(-0.1, -1.0, color),
+        ],
+        indices: vec![2, 4, 1, 0, 4, 3, 2, 5, 4, 0, 1, 4, 8, 10, 7, 6, 10, 9, 8, 11, 10, 6, 7, 10]
+    }
+}

+ 18 - 0
bin/app/src/shape/switch.rs

@@ -0,0 +1,18 @@
+use crate::{mesh::Color, ui::{VectorShape, ShapeVertex}};
+pub fn create_switch(color: Color) -> VectorShape {
+    VectorShape {
+        verts: vec![
+            ShapeVertex::from_xy(-1.1, -0.5, color),
+            ShapeVertex::from_xy(1.7, -0.2, color),
+            ShapeVertex::from_xy(-1.7, -0.2, color),
+            ShapeVertex::from_xy(0.9, -0.5, color),
+            ShapeVertex::from_xy(0.3, -1.4, color),
+            ShapeVertex::from_xy(1.1, 0.5, color),
+            ShapeVertex::from_xy(-1.7, 0.2, color),
+            ShapeVertex::from_xy(1.7, 0.2, color),
+            ShapeVertex::from_xy(-0.9, 0.5, color),
+            ShapeVertex::from_xy(-0.3, 1.4, color),
+        ],
+        indices: vec![3, 2, 1, 8, 7, 6, 1, 4, 3, 3, 0, 2, 8, 5, 7, 6, 9, 8]
+    }
+}

+ 10 - 2
bin/app/src/ui/editbox/mod.rs

@@ -626,12 +626,16 @@ impl EditBox {
     }
 
     async fn insert_char(&self, key: char) {
-        {
+        let text = {
             let mut editable = self.editable.lock().unwrap();
             let mut tmp = [0; 4];
             let key_str = key.encode_utf8(&mut tmp);
             editable.compose(key_str, true);
-        }
+            editable.get_text()
+        };
+
+        let atom = &mut PropertyAtomicGuard::new();
+        self.text.set(atom, text);
 
         self.pause_blinking();
         self.redraw().await;
@@ -691,12 +695,14 @@ impl EditBox {
                 self.delete(0, 1);
                 self.clamp_scroll(atom);
                 self.pause_blinking();
+                self.text.set(atom, self.editable.lock().unwrap().get_text());
                 self.redraw().await;
             }
             KeyCode::Backspace => {
                 self.delete(1, 0);
                 self.clamp_scroll(atom);
                 self.pause_blinking();
+                self.text.set(atom, self.editable.lock().unwrap().get_text());
                 self.redraw().await;
             }
             KeyCode::Home => {
@@ -1273,6 +1279,8 @@ impl UIObject for EditBox {
         // When text has been changed.
         // Cursor and selection might be invalidated.
         async fn reset(self_: Arc<EditBox>) {
+            self_.editable.lock().unwrap().end_compose();
+            self_.editable.lock().unwrap().set_text(self_.text.get(), String::new());
             let atom = &mut PropertyAtomicGuard::new();
             self_.cursor_pos.set(atom, 0);
             self_.selected.clone().set_null(atom, Role::Internal, 0).unwrap();