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

app: remove obsolete editbox, chatedit, and baseedit

darkfi 10 месяцев назад
Родитель
Сommit
01a062e6f8

+ 20 - 18
bin/app/pydrk/api.py

@@ -47,25 +47,27 @@ class SceneNodeType:
     NULL = 0
     ROOT = 1
     WINDOW = 2
-    WINDOW_INPUT = 6
-    KEYBOARD = 7
-    MOUSE = 8
-    LAYER = 3
-    OBJECT = 4
-    VECTOR_ART = 5
+    WINDOW_INPUT = 3
+    KEYBOARD = 4
+    MOUSE = 5
+    LAYER = 6
+    OBJECT = 7
+    VECTOR_ART = 8
     TEXT = 9
-    TEXTURE = 13
-    FONTS = 10
-    FONT = 11
-    PLUGINS = 14
-    PLUGIN = 15
-    CHAT_VIEW = 16
-    EDIT_BOX = 17
-    CHAT_EDIT = 18
-    IMAGE = 18
-    BUTTON = 19
-    SETTING_ROOT = 24
-    SETTING = 25
+    TEXTURE = 10
+    FONTS = 11
+    FONT = 12
+    CHAT_VIEW = 13
+    EDIT = 14
+    IMAGE = 15
+    BUTTON = 16
+    SHORTCUT = 17
+    GESTURE = 18
+    EMOJI_PICKER = 19
+    SETTING_ROOT = 20
+    SETTING = 21
+    PLUGINS = 100
+    PLUGIN = 101
 
 class PropertyType:
     NULL = 0

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

@@ -225,119 +225,6 @@ pub fn create_text(name: &str) -> SceneNode {
     node
 }
 
-#[allow(dead_code)]
-pub fn create_editbox(name: &str) -> SceneNode {
-    t!("create_editbox({name})");
-    let mut node = SceneNode::new(name, SceneNodeType::EditBox);
-
-    let mut prop = Property::new("is_active", PropertyType::Bool, PropertySubType::Null);
-    prop.set_ui_text("Is Active", "An active EditBox can be focused");
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("is_focused", PropertyType::Bool, PropertySubType::Null);
-    prop.set_ui_text("Is Focused", "A focused EditBox receives input");
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("rect", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_array_len(4);
-    prop.allow_exprs();
-    node.add_property(prop).unwrap();
-
-    let prop = Property::new("baseline", PropertyType::Float32, PropertySubType::Pixel);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("scroll", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_range_f32(0., f32::MAX);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("scroll_speed", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_ui_text("Scroll Speed", "Scrolling speed");
-    prop.set_defaults_f32(vec![4.]).unwrap();
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("cursor_pos", PropertyType::Uint32, PropertySubType::Pixel);
-    prop.set_range_u32(0, u32::MAX);
-    node.add_property(prop).unwrap();
-
-    let prop = Property::new("font_size", PropertyType::Float32, PropertySubType::Pixel);
-    node.add_property(prop).unwrap();
-
-    let prop = Property::new("text", PropertyType::Str, PropertySubType::Null);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("text_color", PropertyType::Float32, PropertySubType::Color);
-    prop.set_array_len(4);
-    prop.set_range_f32(0., 1.);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("text_hi_color", PropertyType::Float32, PropertySubType::Color);
-    prop.set_array_len(4);
-    prop.set_range_f32(0., 1.);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("cursor_color", PropertyType::Float32, PropertySubType::Color);
-    prop.set_array_len(4);
-    prop.set_range_f32(0., 1.);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("cursor_width", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_defaults_f32(vec![2.]).unwrap();
-    prop.set_range_f32(0., f32::MAX);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("cursor_ascent", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_defaults_f32(vec![10.]).unwrap();
-    prop.set_range_f32(0., f32::MAX);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("cursor_descent", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_range_f32(0., f32::MAX);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("select_ascent", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_defaults_f32(vec![10.]).unwrap();
-    prop.set_range_f32(0., f32::MAX);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("select_descent", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_range_f32(0., f32::MAX);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("hi_bg_color", PropertyType::Float32, PropertySubType::Color);
-    prop.set_array_len(4);
-    prop.set_range_f32(0., 1.);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("selected", PropertyType::Uint32, PropertySubType::Color);
-    prop.set_array_len(2);
-    prop.allow_null_values();
-    prop.set_defaults_null().unwrap();
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("cursor_blink_time", PropertyType::Uint32, PropertySubType::Null);
-    prop.set_defaults_u32(vec![500]).unwrap();
-    prop.set_range_u32(0, u32::MAX);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("cursor_idle_time", PropertyType::Uint32, PropertySubType::Null);
-    prop.set_defaults_u32(vec![150]).unwrap();
-    prop.set_range_u32(0, u32::MAX);
-    node.add_property(prop).unwrap();
-
-    let prop = Property::new("z_index", PropertyType::Uint32, PropertySubType::Null);
-    node.add_property(prop).unwrap();
-
-    let prop = Property::new("priority", PropertyType::Uint32, PropertySubType::Null);
-    node.add_property(prop).unwrap();
-
-    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
-}
-
 pub fn create_baseedit(name: &str) -> SceneNode {
     let mut node = SceneNode::new(name, SceneNodeType::ChatEdit);
 
@@ -514,156 +401,6 @@ pub fn create_multiline_edit(name: &str) -> SceneNode {
     node
 }
 
-pub fn create_chatedit(name: &str) -> SceneNode {
-    t!("create_chatedit({name})");
-    let mut node = SceneNode::new(name, SceneNodeType::ChatEdit);
-
-    let mut prop = Property::new("is_active", PropertyType::Bool, PropertySubType::Null);
-    prop.set_ui_text("Is Active", "An active EditBox can be focused");
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("is_focused", PropertyType::Bool, PropertySubType::Null);
-    prop.set_ui_text("Is Focused", "A focused EditBox receives input");
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("height_range", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_ui_text("Min/Max Height", "Minimum and Maximum height");
-    prop.set_range_f32(0., f32::MAX);
-    prop.set_array_len(2);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("content_height", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_ui_text("Content Height", "The actual text's inner height");
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("rect", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_array_len(4);
-    prop.allow_exprs();
-    node.add_property(prop).unwrap();
-
-    let prop = Property::new("baseline", PropertyType::Float32, PropertySubType::Pixel);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("lineheight", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_ui_text("Line Height", "Line height/lead (em)");
-    prop.set_defaults_f32(vec![1.2]).unwrap();
-    prop.set_range_f32(0., f32::MAX);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("scroll", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_range_f32(0., f32::MAX);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("scroll_speed", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_ui_text("Scroll Speed", "Scrolling speed");
-    prop.set_defaults_f32(vec![4.]).unwrap();
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("padding", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_ui_text("Inner Padding", "Top and bottom padding inside");
-    prop.set_range_f32(0., f32::MAX);
-    prop.set_array_len(2);
-    node.add_property(prop).unwrap();
-
-    let prop = Property::new("font_size", PropertyType::Float32, PropertySubType::Pixel);
-    node.add_property(prop).unwrap();
-
-    let prop = Property::new("text", PropertyType::Str, PropertySubType::Null);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("text_color", PropertyType::Float32, PropertySubType::Color);
-    prop.set_array_len(4);
-    prop.set_range_f32(0., 1.);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("text_hi_color", PropertyType::Float32, PropertySubType::Color);
-    prop.set_array_len(4);
-    prop.set_range_f32(0., 1.);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("text_cmd_color", PropertyType::Float32, PropertySubType::Color);
-    prop.set_array_len(4);
-    prop.set_range_f32(0., 1.);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("cursor_color", PropertyType::Float32, PropertySubType::Color);
-    prop.set_array_len(4);
-    prop.set_range_f32(0., 1.);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("cursor_width", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_defaults_f32(vec![2.]).unwrap();
-    prop.set_range_f32(0., f32::MAX);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("cursor_ascent", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_defaults_f32(vec![10.]).unwrap();
-    prop.set_range_f32(0., f32::MAX);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("cursor_descent", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_range_f32(0., f32::MAX);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("select_ascent", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_defaults_f32(vec![10.]).unwrap();
-    prop.set_range_f32(0., f32::MAX);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("select_descent", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_range_f32(0., f32::MAX);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("handle_descent", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_range_f32(0., f32::MAX);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("hi_bg_color", PropertyType::Float32, PropertySubType::Color);
-    prop.set_array_len(4);
-    prop.set_range_f32(0., 1.);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("cmd_bg_color", PropertyType::Float32, PropertySubType::Color);
-    prop.set_array_len(4);
-    prop.set_range_f32(0., 1.);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("select_text", PropertyType::Str, PropertySubType::Null);
-    prop.allow_null_values();
-    prop.set_defaults_null().unwrap();
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("cursor_blink_time", PropertyType::Uint32, PropertySubType::Null);
-    prop.set_defaults_u32(vec![500]).unwrap();
-    prop.set_range_u32(0, u32::MAX);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("cursor_idle_time", PropertyType::Uint32, PropertySubType::Null);
-    prop.set_defaults_u32(vec![150]).unwrap();
-    prop.set_range_u32(0, u32::MAX);
-    node.add_property(prop).unwrap();
-
-    let prop = Property::new("z_index", PropertyType::Uint32, PropertySubType::Null);
-    node.add_property(prop).unwrap();
-
-    let prop = Property::new("priority", PropertyType::Uint32, PropertySubType::Null);
-    node.add_property(prop).unwrap();
-
-    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.add_signal("focus_request", "Request to gain focus", vec![]).unwrap();
-    node.add_signal("paste_request", "Request to show paste dialog", vec![]).unwrap();
-
-    // Used by emoji_picker
-    node.add_method("insert_text", vec![("text", "Text", CallArgType::Str)], None).unwrap();
-    node.add_method("focus", vec![], None).unwrap();
-    node.add_method("unfocus", vec![], None).unwrap();
-
-    node
-}
-
 pub fn create_chatview(name: &str) -> SceneNode {
     t!("create_chatview({name})");
     let mut node = SceneNode::new(name, SceneNodeType::ChatView);

+ 13 - 8
bin/app/src/app/schema/chat.rs

@@ -23,8 +23,8 @@ use std::time::UNIX_EPOCH;
 use crate::{
     app::{
         node::{
-            create_button, create_chatedit, create_chatview, create_emoji_picker, create_layer,
-            create_shortcut, create_text, create_vector_art,
+            create_button, create_chatview, create_emoji_picker, create_layer,
+            create_multiline_edit, create_shortcut, create_text, create_vector_art,
         },
         App,
     },
@@ -38,8 +38,8 @@ use crate::{
     scene::{Pimpl, SceneNodePtr, Slot},
     shape,
     ui::{
-        chatview, emoji_picker, Button, ChatEdit, ChatView, EmojiPicker, Layer, Shortcut, Text,
-        VectorArt, VectorShape,
+        chatview, emoji_picker, BaseEdit, BaseEditType, Button, ChatView, EmojiPicker, Layer,
+        Shortcut, Text, VectorArt, VectorShape,
     },
     util::{i18n::I18nBabelFish, unixtime},
 };
@@ -665,7 +665,7 @@ pub async fn make(
     layer_node.link(node);
 
     // Text edit
-    let node = create_chatedit("editz");
+    let node = create_multiline_edit("editz");
     node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
     node.set_property_bool(atom, Role::App, "is_focused", true).unwrap();
 
@@ -686,7 +686,9 @@ pub async fn make(
 
     let prop = node.get_property("padding").unwrap();
     prop.set_f32(atom, Role::App, 0, TEXTBAR_BASELINE * 0.4).unwrap();
-    prop.set_f32(atom, Role::App, 1, TEXTBAR_BASELINE / 2.).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, 0.).unwrap();
 
     node.set_property_f32(atom, Role::App, "baseline", TEXTBAR_BASELINE).unwrap();
     node.set_property_f32(atom, Role::App, "font_size", FONTSIZE * 1.2).unwrap();
@@ -773,8 +775,11 @@ pub async fn make(
     //});
     //tasks.push(task);
 
-    let node =
-        node.setup(|me| ChatEdit::new(me, window_scale.clone(), app.render_api.clone())).await;
+    let node = node
+        .setup(|me| {
+            BaseEdit::new(me, window_scale.clone(), app.render_api.clone(), BaseEditType::MultiLine)
+        })
+        .await;
     let chatedit_node = node.clone();
     layer_node.link(node);
 

+ 3 - 31
bin/app/src/app/schema/test.rs

@@ -19,8 +19,8 @@
 use crate::{
     app::{
         node::{
-            create_chatedit, create_editbox, create_layer, create_multiline_edit,
-            create_singleline_edit, create_text, create_vector_art, create_video,
+            create_layer, create_multiline_edit, create_singleline_edit, create_text,
+            create_vector_art, create_video,
         },
         App,
     },
@@ -28,7 +28,7 @@ use crate::{
     mesh::COLOR_PURPLE,
     prop::{PropertyAtomicGuard, PropertyFloat32, Role},
     scene::SceneNodePtr,
-    ui::{BaseEdit, BaseEditType, ChatEdit, EditBox, Layer, Text, VectorArt, VectorShape, Video},
+    ui::{BaseEdit, BaseEditType, Layer, Text, VectorArt, VectorShape, Video},
     util::i18n::I18nBabelFish,
 };
 
@@ -212,34 +212,6 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
         node.setup(|me| VectorArt::new(me, shape, app.render_api.clone(), app.ex.clone())).await;
     layer_node.link(node);
 
-    // Another debug tool for the chatedit
-    let node = create_vector_art("debugtool-chatedit");
-    let prop = node.get_property("rect").unwrap();
-    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
-    prop.set_f32(atom, Role::App, 1, 300. - 5.).unwrap();
-    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
-    prop.set_f32(atom, Role::App, 3, 400. + 10.).unwrap();
-    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
-
-    let mut shape = VectorShape::new();
-    shape.add_filled_box(
-        expr::const_f32(0.),
-        expr::const_f32(0.),
-        expr::load_var("w"),
-        expr::const_f32(5.),
-        [0., 1., 1., 1.],
-    );
-    shape.add_filled_box(
-        expr::const_f32(0.),
-        cc.compile("h - 5").unwrap(),
-        expr::load_var("w"),
-        expr::load_var("h"),
-        [0., 1., 1., 1.],
-    );
-    let node =
-        node.setup(|me| VectorArt::new(me, shape, app.render_api.clone(), app.ex.clone())).await;
-    layer_node.link(node);
-
     // Create KING GNU!
     let node = create_image("king");
     let prop = node.get_property("rect").unwrap();

+ 19 - 25
bin/app/src/scene.rs

@@ -98,29 +98,25 @@ pub enum SceneNodeType {
     Null = 0,
     Root = 1,
     Window = 2,
-    WindowInput = 6,
-    Keyboard = 7,
-    Mouse = 8,
-    Layer = 3,
-    Object = 4,
-    VectorArt = 5,
+    WindowInput = 3,
+    Keyboard = 4,
+    Mouse = 5,
+    Layer = 6,
+    Object = 7,
+    VectorArt = 8,
     Text = 9,
-    Texture = 13,
-    Fonts = 10,
-    Font = 11,
-    //Plugins = 14,
-    //Plugin = 15,
-    ChatView = 16,
-    EditBox = 17,
-    ChatEdit = 18,
-    Edit = 26,
-    Image = 19,
-    Button = 20,
-    Shortcut = 21,
-    Gesture = 22,
-    EmojiPicker = 23,
-    SettingRoot = 24,
-    Setting = 25,
+    Texture = 10,
+    Fonts = 11,
+    Font = 12,
+    ChatView = 13,
+    Edit = 14,
+    Image = 15,
+    Button = 16,
+    Shortcut = 17,
+    Gesture = 18,
+    EmojiPicker = 19,
+    SettingRoot = 20,
+    Setting = 21,
     PluginRoot = 100,
     Plugin = 101,
 }
@@ -541,10 +537,8 @@ pub enum Pimpl {
     Layer(ui::LayerPtr),
     VectorArt(ui::VectorArtPtr),
     Text(ui::TextPtr),
-    EditBox(ui::EditBoxPtr),
-    ChatEdit(ui::ChatEditPtr),
-    ChatView(ui::ChatViewPtr),
     Edit(ui::BaseEditPtr),
+    ChatView(ui::ChatViewPtr),
     Image(ui::ImagePtr),
     Video(ui::VideoPtr),
     Button(ui::ButtonPtr),

+ 0 - 1727
bin/app/src/ui/chatedit.rs

@@ -1,1727 +0,0 @@
-/* This file is part of DarkFi (https://dark.fi)
- *
- * Copyright (C) 2020-2025 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 async_lock::Mutex as AsyncMutex;
-use async_trait::async_trait;
-use darkfi::system::msleep;
-use darkfi_serial::Decodable;
-use miniquad::{KeyCode, KeyMods, MouseButton, TouchPhase};
-use parking_lot::Mutex as SyncMutex;
-use rand::{rngs::OsRng, Rng};
-use std::{
-    io::Cursor,
-    ops::{Deref, DerefMut},
-    sync::{
-        atomic::{AtomicBool, Ordering},
-        Arc, Weak,
-    },
-};
-
-#[cfg(target_os = "android")]
-use crate::AndroidSuggestEvent;
-use crate::{
-    gfx::{gfxtag, DrawCall, DrawInstruction, DrawMesh, Point, Rectangle, RenderApi, Vertex},
-    mesh::MeshBuilder,
-    prop::{
-        BatchGuardId, BatchGuardPtr, PropertyAtomicGuard, PropertyBool, PropertyColor,
-        PropertyFloat32, PropertyPtr, PropertyRect, PropertyStr, PropertyUint32, Role,
-    },
-    scene::{MethodCallSub, Pimpl, SceneNodePtr, SceneNodeWeak},
-    text2::{self, Editor},
-    util::unixtime,
-    ExecutorPtr,
-};
-
-use super::{
-    baseedit::{
-        repeat::{PressedKey, PressedKeysSmoothRepeat},
-        ALLOWED_KEYCODES, DISALLOWED_CHARS,
-    },
-    DrawUpdate, OnModify, UIObject,
-};
-
-/// The travel threshold on long hold select before activating select.
-const HOLD_TRAVEL_THRESHOLD_SQ: f32 = 100.;
-/// How long to hold before select is enabled in ms.
-const HOLD_ENABLE_TIME: u128 = 500;
-
-/// Minimum dist to update scroll when finger scrolling.
-/// Avoid updating too much makes scrolling smoother.
-const VERT_SCROLL_UPDATE_INC: f32 = 1.;
-
-macro_rules! d { ($($arg:tt)*) => { debug!(target: "ui::chatedit", $($arg)*); } }
-macro_rules! t { ($($arg:tt)*) => { trace!(target: "ui::chatedit", $($arg)*); } }
-
-#[derive(Clone)]
-enum TouchStateAction {
-    Inactive,
-    Started { pos: Point, instant: std::time::Instant },
-    StartSelect,
-    Select,
-    DragSelectHandle { side: isize },
-    ScrollVert { start_pos: Point, scroll_start: f32 },
-    SetCursorPos,
-}
-
-struct TouchInfo {
-    state: TouchStateAction,
-    scroll: PropertyFloat32,
-}
-
-impl TouchInfo {
-    fn new(scroll: PropertyFloat32) -> Self {
-        Self { state: TouchStateAction::Inactive, scroll }
-    }
-
-    fn start(&mut self, pos: Point) {
-        debug!(target: "ui::chatedit::touch", "start touch: Started state");
-        self.state = TouchStateAction::Started { pos, instant: std::time::Instant::now() };
-    }
-
-    fn stop(&mut self) -> TouchStateAction {
-        debug!(target: "ui::chatedit::touch", "stop touch: Inactive state");
-        std::mem::replace(&mut self.state, TouchStateAction::Inactive)
-    }
-
-    fn update(&mut self, pos: &Point) {
-        match &self.state {
-            TouchStateAction::Started { pos: start_pos, instant } => {
-                let travel_dist_sq = pos.dist_sq(*start_pos);
-                let grad = (pos.y - start_pos.y) / (pos.x - start_pos.x);
-                let elapsed = instant.elapsed().as_millis();
-                //debug!(target: "ui::chatedit::touch", "TouchInfo::update() [travel_dist_sq={travel_dist_sq}, grad={grad}]");
-
-                if travel_dist_sq < HOLD_TRAVEL_THRESHOLD_SQ {
-                    if elapsed > HOLD_ENABLE_TIME {
-                        debug!(target: "ui::chatedit::touch", "update touch state: Started -> StartSelect");
-                        self.state = TouchStateAction::StartSelect;
-                    }
-                } else if grad.abs() > 0.5 {
-                    // Vertical movement
-                    debug!(target: "ui::chatedit::touch", "update touch state: Started -> ScrollVert");
-                    let scroll_start = self.scroll.get();
-                    self.state =
-                        TouchStateAction::ScrollVert { start_pos: *start_pos, scroll_start };
-                } else {
-                    // Horizontal movement
-                    debug!(target: "ui::chatedit::touch", "update touch state: Started -> SetCursorPos");
-                    self.state = TouchStateAction::SetCursorPos;
-                }
-            }
-            _ => {}
-        }
-    }
-}
-
-/*
-impl std::fmt::Debug for Editor {
-    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
-        let mut changes = vec![];
-        let sel = self.editor.raw_selection();
-        if sel.is_collapsed() {
-            let cursor = sel.focus().index();
-            changes.push((cursor, '|'));
-        } else {
-            let sel = sel.text_range();
-            changes.push((sel.start, '{'));
-            changes.push((sel.end, '}'));
-        }
-
-        if let Some(compose) = self.editor.compose() {
-            changes.push((compose.start, '['));
-            changes.push((compose.end, ']'));
-        }
-
-        changes.sort_by(|a, b| b.0.cmp(&a.0));
-
-        write!(f, "'")?;
-        let mut buffer = self.editor.raw_text();
-        for (byte_idx, c) in buffer.char_indices() {
-            while let Some((idx, d)) = changes.last() {
-                if *idx > byte_idx {
-                    break
-                }
-
-                write!(f, "{}", d)?;
-                let _ = changes.pop();
-            }
-
-            write!(f, "{}", c)?;
-        }
-        write!(f, "'")
-    }
-}
-*/
-
-struct EditorHandle<'a> {
-    guard: async_lock::MutexGuard<'a, Option<Editor>>,
-}
-
-impl<'a> Deref for EditorHandle<'a> {
-    type Target = Editor;
-
-    fn deref(&self) -> &Self::Target {
-        self.guard.as_ref().unwrap()
-    }
-}
-impl<'a> DerefMut for EditorHandle<'a> {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        self.guard.as_mut().unwrap()
-    }
-}
-
-pub type ChatEditPtr = Arc<ChatEdit>;
-
-pub struct ChatEdit {
-    node: SceneNodeWeak,
-    tasks: SyncMutex<Vec<smol::Task<()>>>,
-    render_api: RenderApi,
-    key_repeat: SyncMutex<PressedKeysSmoothRepeat>,
-
-    // Moves the draw cursor and applies scroll
-    root_dc_key: u64,
-    phone_select_handle_dc_key: u64,
-    // Applies the clipping view
-    content_dc_key: u64,
-    select_dc_key: u64,
-    text_dc_key: u64,
-    cursor_dc_key: u64,
-    cursor_mesh: SyncMutex<Option<DrawMesh>>,
-
-    is_active: PropertyBool,
-    is_focused: PropertyBool,
-    min_height: PropertyFloat32,
-    max_height: PropertyFloat32,
-    content_height: PropertyFloat32,
-    rect: PropertyRect,
-    baseline: PropertyFloat32,
-    lineheight: PropertyFloat32,
-    scroll: PropertyFloat32,
-    scroll_speed: PropertyFloat32,
-    padding: PropertyPtr,
-    font_size: PropertyFloat32,
-    text: PropertyStr,
-    text_color: PropertyColor,
-    text_hi_color: PropertyColor,
-    //text_cmd_color: PropertyColor,
-    cursor_color: PropertyColor,
-    cursor_width: PropertyFloat32,
-    cursor_ascent: PropertyFloat32,
-    cursor_descent: PropertyFloat32,
-    cursor_blink_time: PropertyUint32,
-    cursor_idle_time: PropertyUint32,
-    hi_bg_color: PropertyColor,
-    //cmd_bg_color: PropertyColor,
-    select_ascent: PropertyFloat32,
-    select_descent: PropertyFloat32,
-    handle_descent: PropertyFloat32,
-    select_text: PropertyPtr,
-    z_index: PropertyUint32,
-    priority: PropertyUint32,
-    debug: PropertyBool,
-
-    mouse_btn_held: AtomicBool,
-    cursor_is_visible: AtomicBool,
-    blink_is_paused: AtomicBool,
-    /// Used to explicitly hide the cursor. Must be manually re-enabled.
-    hide_cursor: AtomicBool,
-
-    touch_info: SyncMutex<TouchInfo>,
-    is_phone_select: AtomicBool,
-
-    window_scale: PropertyFloat32,
-    parent_rect: SyncMutex<Option<Rectangle>>,
-    is_mouse_hover: AtomicBool,
-
-    editor: AsyncMutex<Option<Editor>>,
-}
-
-impl ChatEdit {
-    pub async fn new(
-        node: SceneNodeWeak,
-        window_scale: PropertyFloat32,
-        render_api: RenderApi,
-    ) -> Pimpl {
-        t!("ChatEdit::new()");
-
-        let node_ref = &node.upgrade().unwrap();
-        let is_active = PropertyBool::wrap(node_ref, Role::Internal, "is_active", 0).unwrap();
-        let is_focused = PropertyBool::wrap(node_ref, Role::Internal, "is_focused", 0).unwrap();
-        let min_height =
-            PropertyFloat32::wrap(node_ref, Role::Internal, "height_range", 0).unwrap();
-        let max_height =
-            PropertyFloat32::wrap(node_ref, Role::Internal, "height_range", 1).unwrap();
-        let content_height =
-            PropertyFloat32::wrap(node_ref, Role::Internal, "content_height", 0).unwrap();
-        let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
-        let baseline = PropertyFloat32::wrap(node_ref, Role::Internal, "baseline", 0).unwrap();
-        let lineheight = PropertyFloat32::wrap(node_ref, Role::Internal, "lineheight", 0).unwrap();
-        let scroll = PropertyFloat32::wrap(node_ref, Role::Internal, "scroll", 0).unwrap();
-        let scroll_speed =
-            PropertyFloat32::wrap(node_ref, Role::Internal, "scroll_speed", 0).unwrap();
-        let padding = node_ref.get_property("padding").unwrap();
-        let font_size = PropertyFloat32::wrap(node_ref, Role::Internal, "font_size", 0).unwrap();
-        let text = PropertyStr::wrap(node_ref, Role::Internal, "text", 0).unwrap();
-        let text_color = PropertyColor::wrap(node_ref, Role::Internal, "text_color").unwrap();
-        let text_hi_color = PropertyColor::wrap(node_ref, Role::Internal, "text_hi_color").unwrap();
-        //let text_cmd_color =
-        //    PropertyColor::wrap(node_ref, Role::Internal, "text_cmd_color").unwrap();
-        let cursor_color = PropertyColor::wrap(node_ref, Role::Internal, "cursor_color").unwrap();
-        let cursor_width =
-            PropertyFloat32::wrap(node_ref, Role::Internal, "cursor_width", 0).unwrap();
-        let cursor_ascent =
-            PropertyFloat32::wrap(node_ref, Role::Internal, "cursor_ascent", 0).unwrap();
-        let cursor_descent =
-            PropertyFloat32::wrap(node_ref, Role::Internal, "cursor_descent", 0).unwrap();
-        let hi_bg_color = PropertyColor::wrap(node_ref, Role::Internal, "hi_bg_color").unwrap();
-        //let cmd_bg_color = PropertyColor::wrap(node_ref, Role::Internal, "cmd_bg_color").unwrap();
-        let select_ascent =
-            PropertyFloat32::wrap(node_ref, Role::Internal, "select_ascent", 0).unwrap();
-        let select_descent =
-            PropertyFloat32::wrap(node_ref, Role::Internal, "select_descent", 0).unwrap();
-        let handle_descent =
-            PropertyFloat32::wrap(node_ref, Role::Internal, "handle_descent", 0).unwrap();
-        let select_text = node_ref.get_property("select_text").unwrap();
-        let cursor_blink_time =
-            PropertyUint32::wrap(node_ref, Role::Internal, "cursor_blink_time", 0).unwrap();
-        let cursor_idle_time =
-            PropertyUint32::wrap(node_ref, Role::Internal, "cursor_idle_time", 0).unwrap();
-        let z_index = PropertyUint32::wrap(node_ref, Role::Internal, "z_index", 0).unwrap();
-        let priority = PropertyUint32::wrap(node_ref, Role::Internal, "priority", 0).unwrap();
-        let debug = PropertyBool::wrap(node_ref, Role::Internal, "debug", 0).unwrap();
-
-        let self_ = Arc::new(Self {
-            node,
-            tasks: SyncMutex::new(vec![]),
-            render_api,
-            key_repeat: SyncMutex::new(PressedKeysSmoothRepeat::new(400, 50)),
-
-            root_dc_key: OsRng.gen(),
-            phone_select_handle_dc_key: OsRng.gen(),
-            content_dc_key: OsRng.gen(),
-            select_dc_key: OsRng.gen(),
-            text_dc_key: OsRng.gen(),
-            cursor_dc_key: OsRng.gen(),
-            cursor_mesh: SyncMutex::new(None),
-
-            is_active,
-            is_focused,
-            min_height,
-            max_height,
-            content_height,
-            rect,
-            baseline,
-            lineheight: lineheight.clone(),
-            scroll: scroll.clone(),
-            scroll_speed,
-            padding,
-            font_size: font_size.clone(),
-            text: text.clone(),
-            text_color: text_color.clone(),
-            text_hi_color,
-            //text_cmd_color,
-            cursor_color,
-            cursor_width,
-            cursor_ascent,
-            cursor_descent,
-            cursor_blink_time,
-            cursor_idle_time,
-            hi_bg_color,
-            //cmd_bg_color,
-            select_ascent,
-            select_descent,
-            handle_descent,
-            select_text,
-            z_index,
-            priority,
-            debug,
-
-            mouse_btn_held: AtomicBool::new(false),
-            cursor_is_visible: AtomicBool::new(true),
-            blink_is_paused: AtomicBool::new(false),
-            hide_cursor: AtomicBool::new(false),
-
-            touch_info: SyncMutex::new(TouchInfo::new(scroll)),
-            is_phone_select: AtomicBool::new(false),
-
-            window_scale: window_scale.clone(),
-            parent_rect: SyncMutex::new(None),
-            is_mouse_hover: AtomicBool::new(false),
-
-            editor: AsyncMutex::new(None),
-        });
-
-        Pimpl::ChatEdit(self_)
-    }
-
-    fn node(&self) -> SceneNodePtr {
-        self.node.upgrade().unwrap()
-    }
-
-    fn padding_top(&self) -> f32 {
-        self.padding.get_f32(0).unwrap()
-    }
-    fn padding_bottom(&self) -> f32 {
-        self.padding.get_f32(1).unwrap()
-    }
-
-    fn abs_to_local(&self, point: &mut Point) {
-        let rect = self.rect.get();
-        *point -= rect.pos();
-        *point -= self.inner_pos();
-        point.y += self.scroll.get();
-    }
-
-    /// Inner position used for rendering
-    fn inner_pos(&self) -> Point {
-        let pad_top = self.padding_top();
-        let pad_bot = self.padding_bottom();
-        let content_height = self.content_height.get();
-        let outer_height = content_height + pad_top + pad_bot;
-        let rect_h = self.rect.get_height();
-        let mut inner_pos = Point::zero();
-        if outer_height < rect_h {
-            // Min was applied to clip. Center content inside the rect.
-            inner_pos.y = (rect_h - content_height) / 2.;
-        } else {
-            inner_pos.y = pad_top;
-        }
-        inner_pos
-    }
-
-    /// Maximum allowed scroll value
-    /// * `content_height` measures the height of the actual content.
-    /// * `outer_height` applies the inner padding.
-    /// * `rect_h` then clips the `outer_height` to min/max values.
-    /// We only allow scrolling when max clipping has been applied.
-    fn max_scroll(&self) -> f32 {
-        let content_height = self.content_height.get();
-        let outer_height = content_height + self.padding_top() + self.padding_bottom();
-        let rect_h = self.rect.get_height();
-        //t!("max_scroll content_height={content_height}, rect_h={rect_h}");
-        (outer_height - rect_h).max(0.)
-    }
-
-    /// Gets the real cursor pos within the rect.
-    async fn get_cursor_pos(&self) -> Point {
-        // This is the position within the content.
-        let cursor_pos = self.lock_editor().await.get_cursor_pos();
-        // Apply the inner padding
-        cursor_pos + self.inner_pos()
-    }
-
-    /// Lazy-initializes the editor and returns a handle to it
-    async fn lock_editor<'a>(&'a self) -> EditorHandle<'a> {
-        EditorHandle { guard: self.editor.lock().await }
-    }
-
-    fn regen_cursor_mesh(&self) -> DrawMesh {
-        let cursor_width = self.cursor_width.get();
-        let cursor_ascent = self.cursor_ascent.get();
-        let cursor_descent = self.cursor_descent.get();
-        let baseline = self.baseline.get();
-
-        let cursor_rect = Rectangle {
-            x: 0.,
-            y: baseline - cursor_ascent,
-            w: cursor_width,
-            h: cursor_ascent + cursor_descent,
-        };
-        let cursor_color = self.cursor_color.get();
-
-        let mut mesh = MeshBuilder::new(gfxtag!("chatedit_cursor"));
-        mesh.draw_filled_box(&cursor_rect, cursor_color);
-        mesh.alloc(&self.render_api).draw_untextured()
-    }
-
-    fn draw_phone_select_handle(&self, mesh: &mut MeshBuilder, pos: Point, side: f32) {
-        let scroll = self.scroll.get();
-        let baseline = self.baseline.get();
-        let select_ascent = self.select_ascent.get();
-        let handle_descent = self.handle_descent.get();
-        let color = self.text_hi_color.get();
-        // Transparent for fade
-        let mut color_trans = color.clone();
-        color_trans[3] = 0.;
-
-        let x = pos.x;
-        let mut y = pos.y + baseline;
-
-        if y - scroll < 0. {
-            return
-        }
-
-        // Vertical line downwards. We use this instead of draw_box() so we have a fade.
-        let verts = vec![
-            Vertex { pos: [x - side * 1., y - select_ascent], color: color_trans, uv: [0., 0.] },
-            Vertex { pos: [x + side * 4., y - select_ascent], color: color_trans, uv: [0., 0.] },
-            Vertex { pos: [x - side * 1., y + handle_descent + 5.], color, uv: [0., 0.] },
-            Vertex { pos: [x + side * 4., y + handle_descent + 5.], color, uv: [0., 0.] },
-        ];
-        let indices = vec![0, 2, 1, 1, 2, 3];
-        mesh.append(verts, indices);
-
-        y += handle_descent;
-
-        // The arrow itself.
-        // Go anti-clockwise
-        let verts = vec![
-            Vertex { pos: [x, y], color, uv: [0., 0.] },
-            Vertex { pos: [x, y + 50.], color, uv: [0., 0.] },
-            Vertex { pos: [x + side * 30., y + 50.], color, uv: [0., 0.] },
-            Vertex { pos: [x + side * 50., y + 25.], color, uv: [0., 0.] },
-        ];
-        let indices = vec![0, 1, 2, 0, 2, 3];
-        mesh.append(verts, indices);
-    }
-
-    async fn change_focus(self: Arc<Self>, batch: BatchGuardPtr) {
-        if !self.is_active.get() {
-            return
-        }
-        t!("Focus changed");
-
-        let atom = &mut batch.spawn();
-        // Cursor visibility will change so just redraw everything lol
-        self.redraw(atom).await;
-    }
-
-    async fn handle_shortcut(
-        &self,
-        key: char,
-        mods: &KeyMods,
-        atom: &mut PropertyAtomicGuard,
-    ) -> bool {
-        t!("handle_shortcut({:?}, {:?})", key, mods);
-
-        #[cfg(not(target_os = "macos"))]
-        let action_mod = mods.ctrl;
-
-        #[cfg(target_os = "macos")]
-        let action_mod = mods.logo;
-
-        match key {
-            'a' => {
-                if action_mod {
-                    let mut txt_ctx = text2::TEXT_CTX.get().await;
-                    let mut editor = self.lock_editor().await;
-                    let mut drv = editor.driver(&mut txt_ctx).unwrap();
-
-                    drv.select_all();
-                    if let Some(seltext) = editor.selected_text() {
-                        self.select_text.clone().set_str(atom, Role::Internal, 0, seltext).unwrap();
-                    }
-                }
-            }
-            'c' => {
-                if action_mod {
-                    let editor = self.lock_editor().await;
-                    if let Some(txt) = editor.selected_text() {
-                        miniquad::window::clipboard_set(&txt);
-                    }
-                }
-            }
-            'v' => {
-                if action_mod {
-                    if let Some(txt) = miniquad::window::clipboard_get() {
-                        self.insert(&txt, atom).await;
-                        // Maybe insert should call this?
-                        self.apply_cursor_scrolling(atom).await;
-                    }
-                }
-            }
-            _ => return false,
-        }
-
-        self.redraw(atom).await;
-        true
-    }
-
-    async fn handle_key(
-        &self,
-        key: &KeyCode,
-        mods: &KeyMods,
-        atom: &mut PropertyAtomicGuard,
-    ) -> bool {
-        #[cfg(not(target_os = "macos"))]
-        let action_mod = mods.ctrl;
-
-        #[cfg(target_os = "macos")]
-        let action_mod = mods.logo;
-
-        t!("handle_key({:?}, {:?}) action_mod={action_mod}", key, mods);
-
-        let mut txt_ctx = text2::TEXT_CTX.get().await;
-        let mut editor = self.lock_editor().await;
-        let mut drv = editor.driver(&mut txt_ctx).unwrap();
-
-        match key {
-            KeyCode::Left => {
-                if action_mod {
-                    if mods.shift {
-                        drv.select_word_left();
-                    } else {
-                        drv.move_word_left();
-                    }
-                } else if mods.shift {
-                    drv.select_left();
-                } else {
-                    drv.move_left();
-                }
-            }
-            KeyCode::Right => {
-                if action_mod {
-                    if mods.shift {
-                        drv.select_word_right();
-                    } else {
-                        drv.move_word_right();
-                    }
-                } else if mods.shift {
-                    drv.select_right();
-                } else {
-                    drv.move_right();
-                }
-            }
-            KeyCode::Up => {
-                if mods.shift {
-                    drv.select_up();
-                } else {
-                    drv.move_up();
-                }
-            }
-            KeyCode::Down => {
-                if mods.shift {
-                    drv.select_down();
-                } else {
-                    drv.move_down();
-                }
-            }
-            KeyCode::Enter | KeyCode::KpEnter => {
-                if mods.shift {
-                    drv.insert_or_replace_selection("\n");
-                    editor.on_buffer_changed(atom).await;
-                } else {
-                    //let node = self.node.upgrade().unwrap();
-                    //node.trigger("enter_pressed", vec![]).await.unwrap();
-                    return false;
-                }
-            }
-            KeyCode::Delete => {
-                if action_mod {
-                    drv.delete_word();
-                } else {
-                    drv.delete();
-                }
-                editor.on_buffer_changed(atom).await;
-            }
-            KeyCode::Backspace => {
-                if action_mod {
-                    drv.backdelete_word();
-                } else {
-                    drv.backdelete();
-                }
-                editor.on_buffer_changed(atom).await;
-            }
-            KeyCode::Home => {
-                if action_mod {
-                    if mods.shift {
-                        drv.select_to_text_start();
-                    } else {
-                        drv.move_to_text_start();
-                    }
-                } else if mods.shift {
-                    drv.select_to_line_start();
-                } else {
-                    drv.move_to_line_start();
-                }
-            }
-            KeyCode::End => {
-                if action_mod {
-                    if mods.shift {
-                        drv.select_to_text_end();
-                    } else {
-                        drv.move_to_text_end();
-                    }
-                } else if mods.shift {
-                    drv.select_to_line_end();
-                } else {
-                    drv.move_to_line_end();
-                }
-            }
-            _ => return false,
-        }
-
-        if let Some(seltext) = editor.selected_text() {
-            self.select_text.clone().set_str(atom, Role::Internal, 0, seltext).unwrap();
-        } else {
-            self.select_text.clone().set_null(atom, Role::Internal, 0).unwrap();
-        }
-
-        drop(editor);
-        drop(txt_ctx);
-
-        self.apply_cursor_scrolling(atom).await;
-        self.pause_blinking();
-        self.redraw(atom).await;
-
-        return true
-    }
-
-    /// This will select the entire word rather than move the cursor to that location
-    async fn start_touch_select(&self, touch_pos: Point, atom: &mut PropertyAtomicGuard) {
-        t!("start_touch_select({touch_pos:?})");
-
-        let mut editor = self.lock_editor().await;
-        editor.select_word_at_point(touch_pos);
-        editor.refresh().await;
-
-        let seltext = editor.selected_text().unwrap();
-        d!("Selected {seltext:?} from {touch_pos:?}");
-        self.select_text.clone().set_str(atom, Role::Internal, 0, seltext).unwrap();
-
-        drop(editor);
-
-        // if start != end {
-        self.is_phone_select.store(true, Ordering::Relaxed);
-        self.hide_cursor.store(true, Ordering::Relaxed);
-        // }
-    }
-
-    async fn handle_touch_start(&self, touch_pos: Point) -> bool {
-        t!("handle_touch_start({touch_pos:?})");
-
-        let rect = self.rect.get();
-        if !rect.contains(touch_pos) {
-            t!("rect!cont rect={rect:?}, touch_pos={touch_pos:?}");
-            return false
-        }
-
-        if self.try_handle_drag(touch_pos).await {
-            return true
-        }
-
-        let mut touch_info = self.touch_info.lock();
-        touch_info.start(touch_pos);
-        true
-    }
-
-    async fn get_select_handles(&self) -> Option<(Point, Point)> {
-        let editor = self.lock_editor().await;
-        let layout = editor.layout();
-
-        let sel = editor.selection();
-        if sel.is_collapsed() {
-            assert!(!self.is_phone_select.load(Ordering::Relaxed));
-            return None
-        }
-
-        let first = Rectangle::from(sel.anchor().geometry(layout, 0.)).pos();
-        let last = Rectangle::from(sel.focus().geometry(layout, 0.)).pos();
-        Some((first, last))
-    }
-
-    async fn try_handle_drag(&self, mut touch_pos: Point) -> bool {
-        let Some((mut first, mut last)) = self.get_select_handles().await else { return false };
-
-        self.abs_to_local(&mut touch_pos);
-        t!("localize touch_pos = {touch_pos:?}");
-
-        let baseline = self.baseline.get();
-        let handle_off_y = self.handle_descent.get();
-
-        first.y += baseline + handle_off_y;
-        last.y += baseline + handle_off_y;
-
-        // Are we within range of either one?
-        t!("handle center points = ({first:?}, {last:?})");
-
-        const TOUCH_RADIUS_SQ: f32 = 10_000.;
-
-        let first_dist_sq = first.dist_sq(touch_pos);
-        let last_dist_sq = last.dist_sq(touch_pos);
-
-        let is_first = first_dist_sq <= TOUCH_RADIUS_SQ;
-        let is_last = last_dist_sq <= TOUCH_RADIUS_SQ;
-
-        let mut side = 0;
-
-        if is_first && is_last {
-            // Are we closer to the first or last?
-            // Break the tie
-            if first_dist_sq < last_dist_sq {
-                side = -1;
-            } else {
-                side = 1;
-            }
-        } else if is_first {
-            side = -1;
-        } else if is_last {
-            side = 1;
-        }
-
-        if side != 0 {
-            d!("start touch: DragSelectHandle state [side={side}]");
-            // Set touch_state status to enable begin dragging them
-            let mut touch_info = self.touch_info.lock();
-            touch_info.state = TouchStateAction::DragSelectHandle { side };
-            return true
-        }
-
-        false
-    }
-
-    async fn handle_touch_move(&self, mut touch_pos: Point) -> bool {
-        //t!("handle_touch_move({touch_pos:?})");
-        let atom = &mut self.render_api.make_guard(gfxtag!("ChatEdit::handle_touch_move"));
-        // We must update with non relative touch_pos bcos when doing vertical scrolling
-        // we will modify the scroll, which is used by abs_to_local(), which is used
-        // to then calculate the max scroll. So it ends up jumping around.
-        // We use the abs touch_pos without scroll adjust applied for vert scrolling.
-        let touch_state = {
-            let mut touch_info = self.touch_info.lock();
-            touch_info.update(&touch_pos);
-            touch_info.state.clone()
-        };
-        match &touch_state {
-            TouchStateAction::Inactive => return false,
-            TouchStateAction::StartSelect => {
-                if self.text.get().is_empty() {
-                    self.node().trigger("paste_request", vec![]).await.unwrap();
-                } else {
-                    self.abs_to_local(&mut touch_pos);
-                    self.start_touch_select(touch_pos, atom).await;
-                    self.redraw_select(atom.batch_id).await;
-                }
-                d!("touch state: StartSelect -> Select");
-                self.touch_info.lock().state = TouchStateAction::Select;
-            }
-            TouchStateAction::DragSelectHandle { side } => {
-                let handle_descent = self.handle_descent.get();
-                self.abs_to_local(&mut touch_pos);
-
-                let editor = self.lock_editor().await;
-                let sel = editor.selection();
-
-                assert!(*side == -1 || *side == 1);
-                assert!(self.is_phone_select.load(Ordering::Relaxed));
-                assert!(!sel.is_collapsed());
-
-                let mut pos = touch_pos;
-                // Only allow selecting text that is visible in the box
-                // We do our calcs relative to (0, 0) so bhs = rect_h
-                pos.y -= handle_descent + 25.;
-                //let bhs = wrapped_lines.height();
-                //pos.y = min_f32(pos.y, bhs);
-
-                let mut select = sel.text_range();
-
-                let layout = editor.layout();
-                t!("select  (pre): {:?}", sel.text_range());
-                let cursor = parley::Cursor::from_point(layout, pos.x, pos.y).index();
-                t!("cursor: {cursor}");
-
-                // The selection is NOT allowed to cross over itself.
-                if *side == -1 {
-                    let max_start = sel.focus().previous_visual(layout).index();
-                    t!("side == -1  max={max_start}");
-                    select.start = std::cmp::min(cursor, max_start);
-                } else {
-                    assert_eq!(*side, 1);
-                    let min_end = sel.anchor().next_visual(layout).index();
-                    t!("side == +1  min={min_end}");
-                    select.end = std::cmp::max(cursor, min_end);
-                };
-                t!("set_select({select:?})");
-
-                editor.set_selection(select.start, select.end);
-                drop(editor);
-
-                self.redraw_select(atom.batch_id).await;
-            }
-            TouchStateAction::ScrollVert { start_pos, scroll_start } => {
-                let y_dist = start_pos.y - touch_pos.y;
-                let mut scroll = scroll_start + y_dist;
-                scroll = scroll.clamp(0., self.max_scroll());
-                if (self.scroll.get() - scroll).abs() < VERT_SCROLL_UPDATE_INC {
-                    return true
-                }
-                self.scroll.set(atom, scroll);
-                self.redraw_scroll(atom.batch_id).await;
-            }
-            TouchStateAction::SetCursorPos => {
-                // TBH I can't even see the cursor under my thumb so I'll just
-                // comment this for now.
-            }
-            _ => {}
-        }
-        true
-    }
-    async fn handle_touch_end(&self, atom: &mut PropertyAtomicGuard, mut touch_pos: Point) -> bool {
-        //t!("handle_touch_end({touch_pos:?})");
-        self.abs_to_local(&mut touch_pos);
-
-        let state = self.touch_info.lock().stop();
-        match state {
-            TouchStateAction::Inactive => return false,
-            TouchStateAction::Started { pos: _, instant: _ } | TouchStateAction::SetCursorPos => {
-                self.touch_set_cursor_pos(atom, touch_pos).await;
-                self.redraw(atom).await;
-            }
-            _ => {}
-        }
-
-        self.node().trigger("focus_request", vec![]).await.unwrap();
-
-        true
-    }
-
-    async fn touch_set_cursor_pos(&self, atom: &mut PropertyAtomicGuard, touch_pos: Point) {
-        t!("touch_set_cursor_pos({touch_pos:?})");
-
-        let mut editor = self.lock_editor().await;
-        editor.move_to_pos(touch_pos);
-        editor.refresh().await;
-        drop(editor);
-
-        self.pause_blinking();
-        self.finish_select(atom);
-    }
-
-    fn finish_select(&self, atom: &mut PropertyAtomicGuard) {
-        self.is_phone_select.store(false, Ordering::Relaxed);
-        self.hide_cursor.store(false, Ordering::Relaxed);
-        self.select_text.clone().set_null(atom, Role::Internal, 0).unwrap();
-    }
-
-    /// Whenever the cursor is modified this MUST be called
-    /// to recalculate the scroll y property.
-    async fn apply_cursor_scrolling(&self, atom: &mut PropertyAtomicGuard) {
-        let mut scroll = self.scroll.get();
-        let rect_h = self.rect.get_height();
-        let cursor_y0 = self.get_cursor_pos().await.y;
-        let cursor_h = self.baseline.get() + self.cursor_descent.get();
-        // The bottom
-        let cursor_y1 = cursor_y0 + cursor_h;
-        //t!("apply_cursor_scrolling() cursor = [{cursor_y0}, {cursor_y1}] rect_h={rect_h} scroll={scroll}");
-
-        if cursor_y1 > rect_h + scroll {
-            //t!("  cursor bottom below rect");
-            // We want cursor_y1 = rect_h + scroll by adjusting scroll
-            scroll = cursor_y1 - rect_h;
-            self.scroll.set(atom, scroll);
-        } else if cursor_y0 < scroll {
-            //t!("  cursor top above rect");
-            scroll = cursor_y0;
-            self.scroll.set(atom, scroll);
-        }
-    }
-
-    fn pause_blinking(&self) {
-        self.blink_is_paused.store(true, Ordering::Relaxed);
-        self.cursor_is_visible.store(true, Ordering::Relaxed);
-    }
-
-    async fn redraw(&self, atom: &mut PropertyAtomicGuard) {
-        let trace_id = rand::random();
-        let timest = unixtime();
-        let draw_update = self.make_draw_calls(trace_id, atom).await;
-        self.render_api.replace_draw_calls(atom.batch_id, timest, draw_update.draw_calls);
-    }
-
-    /// Called when scroll changes. Moves content up or down. Nothing more.
-    async fn redraw_scroll(&self, batch_id: BatchGuardId) {
-        let timest = unixtime();
-        let rect = self.rect.get();
-        let scroll = self.scroll.get();
-
-        let phone_sel_instrs = self.regen_phone_select_handle_mesh().await;
-
-        let mut content_instrs = vec![
-            DrawInstruction::ApplyView(rect.with_zero_pos()),
-            DrawInstruction::Move(Point::new(0., -scroll)),
-        ];
-        let mut bg_instrs = self.regen_bg_mesh();
-        content_instrs.append(&mut bg_instrs);
-
-        let draw_main = vec![
-            (
-                self.content_dc_key,
-                DrawCall::new(
-                    content_instrs,
-                    vec![self.text_dc_key, self.cursor_dc_key, self.select_dc_key],
-                    0,
-                    "chatedit_content",
-                ),
-            ),
-            (
-                self.phone_select_handle_dc_key,
-                DrawCall::new(phone_sel_instrs, vec![], 1, "chatedit_phone_sel_scroll"),
-            ),
-        ];
-        self.render_api.replace_draw_calls(batch_id, timest, draw_main);
-    }
-
-    async fn redraw_cursor(&self, batch_id: BatchGuardId) {
-        let timest = unixtime();
-        let instrs = self.get_cursor_instrs().await;
-        let draw_calls = vec![(self.cursor_dc_key, DrawCall::new(instrs, vec![], 2, "curs_redr"))];
-        self.render_api.replace_draw_calls(batch_id, timest, draw_calls);
-    }
-
-    async fn redraw_select(&self, batch_id: BatchGuardId) {
-        let timest = unixtime();
-        let sel_instrs = self.regen_select_mesh().await;
-        let phone_sel_instrs = self.regen_phone_select_handle_mesh().await;
-        let draw_calls = vec![
-            (self.select_dc_key, DrawCall::new(sel_instrs, vec![], 0, "chatedit_sel")),
-            (
-                self.phone_select_handle_dc_key,
-                DrawCall::new(phone_sel_instrs, vec![], 1, "chatedit_phone_sel_redraw_sel"),
-            ),
-        ];
-        self.render_api.replace_draw_calls(batch_id, timest, draw_calls);
-    }
-
-    async fn get_cursor_instrs(&self) -> Vec<DrawInstruction> {
-        if !self.is_focused.get() ||
-            !self.cursor_is_visible.load(Ordering::Relaxed) ||
-            self.hide_cursor.load(Ordering::Relaxed)
-        {
-            return vec![]
-        }
-
-        let cursor_mesh =
-            self.cursor_mesh.lock().get_or_insert_with(|| self.regen_cursor_mesh()).clone();
-
-        vec![DrawInstruction::Move(self.get_cursor_pos().await), DrawInstruction::Draw(cursor_mesh)]
-    }
-
-    fn regen_bg_mesh(&self) -> Vec<DrawInstruction> {
-        if !self.debug.get() {
-            return vec![]
-        }
-
-        let padding_top = self.padding_top();
-        let padding_bottom = self.padding_bottom();
-
-        let mut rect = self.rect.get().with_zero_pos();
-
-        let mut mesh = MeshBuilder::new(gfxtag!("chatedit_bg"));
-        mesh.draw_outline(&rect, [0., 1., 0., 1.], 1.);
-
-        rect.y = padding_top;
-        rect.h -= padding_top + padding_bottom;
-        mesh.draw_outline(&rect, [0., 1., 0., 0.5], 1.);
-
-        vec![DrawInstruction::Draw(mesh.alloc(&self.render_api).draw_untextured())]
-    }
-
-    async fn regen_txt_mesh(&self) -> Vec<DrawInstruction> {
-        let mut instrs = vec![DrawInstruction::Move(self.inner_pos())];
-
-        let editor = self.lock_editor().await;
-        let layout = editor.layout();
-
-        let mut render_instrs =
-            text2::render_layout(layout, &self.render_api, gfxtag!("chatedit_txt_mesh"));
-        instrs.append(&mut render_instrs);
-
-        instrs
-    }
-
-    async fn regen_select_mesh(&self) -> Vec<DrawInstruction> {
-        let mut instrs = vec![DrawInstruction::Move(self.inner_pos())];
-
-        let editor = self.lock_editor().await;
-        let layout = editor.layout();
-
-        let sel = editor.selection();
-        let sel_color = self.hi_bg_color.get();
-        if !sel.is_collapsed() {
-            let mut mesh = MeshBuilder::new(gfxtag!("chatedit_select_mesh"));
-            sel.geometry_with(layout, |rect: parley::Rect, _| {
-                mesh.draw_filled_box(&rect.into(), sel_color);
-            });
-
-            instrs.push(DrawInstruction::Draw(mesh.alloc(&self.render_api).draw_untextured()));
-        }
-
-        instrs
-    }
-
-    async fn regen_phone_select_handle_mesh(&self) -> Vec<DrawInstruction> {
-        if !self.is_phone_select.load(Ordering::Relaxed) {
-            return vec![]
-        }
-        //t!("regen_phone_select_handle_mesh()");
-        let (first, last) = self.get_select_handles().await.unwrap();
-
-        let scroll = self.scroll.get();
-
-        let editor = self.lock_editor().await;
-        let sel = editor.selection();
-        assert!(!sel.is_collapsed());
-
-        let pos = self.inner_pos() + Point::new(0., -scroll);
-
-        // We could cache this and use Move instead but why bother?
-        let mut mesh = MeshBuilder::new(gfxtag!("chatedit_phone_select_handle"));
-        self.draw_phone_select_handle(&mut mesh, first, -1.);
-        self.draw_phone_select_handle(&mut mesh, last, 1.);
-        vec![
-            DrawInstruction::Move(pos),
-            DrawInstruction::Draw(mesh.alloc(&self.render_api).draw_untextured()),
-        ]
-    }
-
-    fn bounded_height(&self, height: f32) -> f32 {
-        height.clamp(self.min_height.get(), self.max_height.get())
-    }
-
-    async fn eval_rect(&self, atom: &mut PropertyAtomicGuard) {
-        let parent_rect = self.parent_rect.lock().clone().unwrap();
-        // First we evaluate the width based off the parent dimensions
-        self.rect
-            .eval_with(
-                atom,
-                vec![2],
-                vec![
-                    ("parent_w".to_string(), parent_rect.w),
-                    ("parent_h".to_string(), parent_rect.h),
-                ],
-            )
-            .unwrap();
-
-        // Use the width to adjust the height calcs
-        let rect_w = self.rect.get_width();
-        let content_height = {
-            let mut editor = self.lock_editor().await;
-            editor.set_width(rect_w);
-            editor.refresh().await;
-            editor.height()
-        };
-        self.content_height.set(atom, content_height);
-        let outer_height = content_height + self.padding_top() + self.padding_bottom();
-        let rect_h = self.bounded_height(outer_height);
-        self.rect.prop().set_f32(atom, Role::Internal, 3, rect_h).unwrap();
-
-        // Finally calculate the position
-        self.rect
-            .eval_with(
-                atom,
-                vec![0, 1],
-                vec![
-                    ("parent_w".to_string(), parent_rect.w),
-                    ("parent_h".to_string(), parent_rect.h),
-                    ("rect_w".to_string(), rect_w),
-                    ("rect_h".to_string(), rect_h),
-                ],
-            )
-            .unwrap();
-    }
-
-    async fn make_draw_calls(&self, _trace_id: u32, atom: &mut PropertyAtomicGuard) -> DrawUpdate {
-        self.eval_rect(atom).await;
-
-        let rect = self.rect.get();
-        let max_scroll = self.max_scroll();
-        let mut scroll = self.scroll.get();
-        if scroll > max_scroll {
-            scroll = max_scroll;
-            self.scroll.set(atom, scroll);
-        }
-
-        let cursor_instrs = self.get_cursor_instrs().await;
-        let txt_instrs = self.regen_txt_mesh().await;
-        let sel_instrs = self.regen_select_mesh().await;
-        let phone_sel_instrs = self.regen_phone_select_handle_mesh().await;
-
-        let mut content_instrs = vec![
-            DrawInstruction::ApplyView(rect.with_zero_pos()),
-            DrawInstruction::Move(Point::new(0., -scroll)),
-        ];
-        let mut bg_instrs = self.regen_bg_mesh();
-        content_instrs.append(&mut bg_instrs);
-
-        // + root (move)
-        // -+ content (apply view)
-        //  └╴select
-        //  └╴text
-        //  └╴cursor
-        // - phone_handle
-
-        // Why do we have such a complicated layout?
-        // Firstly phone select handles should never be clipped. So we must draw them outside
-        // of ApplyView.
-        // Then when adjusting selection, it's slow to redraw everything, so the selection
-        // must be drawn separately.
-        // Lastly the cursor is blinking and that's on top but with clipping.
-
-        DrawUpdate {
-            key: self.root_dc_key,
-            draw_calls: vec![
-                (
-                    self.root_dc_key,
-                    DrawCall::new(
-                        vec![DrawInstruction::Move(rect.pos())],
-                        vec![self.content_dc_key, self.phone_select_handle_dc_key],
-                        self.z_index.get(),
-                        "chatedit_root",
-                    ),
-                ),
-                (
-                    self.content_dc_key,
-                    DrawCall::new(
-                        content_instrs,
-                        vec![self.text_dc_key, self.cursor_dc_key, self.select_dc_key],
-                        0,
-                        "chatedit_content",
-                    ),
-                ),
-                (self.select_dc_key, DrawCall::new(sel_instrs, vec![], 0, "chatedit_sel")),
-                (self.text_dc_key, DrawCall::new(txt_instrs, vec![], 1, "chatedit_text")),
-                (self.cursor_dc_key, DrawCall::new(cursor_instrs, vec![], 2, "chatedit_curs")),
-                (
-                    self.phone_select_handle_dc_key,
-                    DrawCall::new(phone_sel_instrs, vec![], 1, "chatedit_phone_sel"),
-                ),
-            ],
-        }
-    }
-
-    async fn insert(&self, txt: &str, atom: &mut PropertyAtomicGuard) {
-        let mut editor = self.lock_editor().await;
-        editor.insert(txt, atom).await;
-    }
-
-    async fn process_insert_text_method(me: &Weak<Self>, sub: &MethodCallSub) -> bool {
-        let Ok(method_call) = sub.receive().await else {
-            debug!(target: "ui::chatedit", "Event relayer closed");
-            return false
-        };
-
-        t!("method called: insert_line({method_call:?})");
-        assert!(method_call.send_res.is_none());
-
-        fn decode_data(data: &[u8]) -> std::io::Result<String> {
-            let mut cur = Cursor::new(&data);
-            let text = String::decode(&mut cur)?;
-            Ok(text)
-        }
-
-        let Ok(text) = decode_data(&method_call.data) else {
-            error!(target: "ui::chatedit", "insert_text() method invalid arg data");
-            return true
-        };
-
-        let Some(self_) = me.upgrade() else {
-            // Should not happen
-            panic!("self destroyed before insert_text_method_task was stopped!");
-        };
-
-        let atom =
-            &mut self_.render_api.make_guard(gfxtag!("ChatEdit::process_insert_text_method"));
-        self_.insert(&text, atom).await;
-        self_.redraw(atom).await;
-        true
-    }
-
-    async fn process_focus_method(me: &Weak<Self>, sub: &MethodCallSub) -> bool {
-        let Ok(method_call) = sub.receive().await else {
-            debug!(target: "ui::chatedit", "Event relayer closed");
-            return false
-        };
-
-        t!("method called: focus({method_call:?})");
-        assert!(method_call.send_res.is_none());
-        assert!(method_call.data.is_empty());
-
-        let Some(self_) = me.upgrade() else {
-            // Should not happen
-            panic!("self destroyed before insert_text_method_task was stopped!");
-        };
-
-        let editor = self_.lock_editor().await;
-        editor.focus();
-        true
-    }
-    async fn process_unfocus_method(me: &Weak<Self>, sub: &MethodCallSub) -> bool {
-        let Ok(method_call) = sub.receive().await else {
-            debug!(target: "ui::chatedit", "Event relayer closed");
-            return false
-        };
-
-        t!("method called: focus({method_call:?})");
-        assert!(method_call.send_res.is_none());
-        assert!(method_call.data.is_empty());
-
-        let Some(self_) = me.upgrade() else {
-            // Should not happen
-            panic!("self destroyed before insert_text_method_task was stopped!");
-        };
-
-        let editor = self_.lock_editor().await;
-        editor.unfocus();
-        true
-    }
-
-    #[cfg(target_os = "android")]
-    async fn handle_android_event(&self, ev: AndroidSuggestEvent) {
-        t!("handle_android_event({ev:?})");
-        if !self.is_active.get() {
-            return
-        }
-
-        let atom = &mut self.render_api.make_guard(gfxtag!("ChatEdit::handle_android_event"));
-        match ev {
-            AndroidSuggestEvent::Init => {
-                let mut editor = self.lock_editor().await;
-                editor.init();
-                // For debugging select, enable these and set a selection in the editor.
-                //self.is_phone_select.store(true, Ordering::Relaxed);
-                //self.hide_cursor.store(true, Ordering::Relaxed);
-
-                // Debug code if we set text in editor.init()
-                //let atom = &mut PropertyAtomicGuard::new();
-                //editor.on_buffer_changed(atom).await;
-                return
-            }
-            AndroidSuggestEvent::CreateInputConnect => {
-                let mut editor = self.lock_editor().await;
-                editor.setup();
-            }
-            // Destructive text edits
-            AndroidSuggestEvent::ComposeRegion { .. } |
-            AndroidSuggestEvent::FinishCompose |
-            AndroidSuggestEvent::Compose { .. } |
-            AndroidSuggestEvent::DeleteSurroundingText { .. } => {
-                // Any editing will collapse selections
-                self.is_phone_select.store(false, Ordering::Relaxed);
-                self.hide_cursor.store(false, Ordering::Relaxed);
-
-                self.finish_select(atom);
-
-                let mut editor = self.lock_editor().await;
-                editor.on_buffer_changed(atom).await;
-                drop(editor);
-
-                self.apply_cursor_scrolling(atom).await;
-            }
-        }
-
-        // Only redraw once we have the parent_rect
-        // Can happen when we receive an Android event before the canvas is ready
-        if self.parent_rect.lock().is_some() {
-            self.redraw(atom).await;
-        }
-    }
-}
-
-impl Drop for ChatEdit {
-    fn drop(&mut self) {
-        let atom = self.render_api.make_guard(gfxtag!("ChatEdit::drop"));
-        self.render_api.replace_draw_calls(
-            atom.batch_id,
-            unixtime(),
-            vec![(self.text_dc_key, Default::default())],
-        );
-    }
-}
-
-#[async_trait]
-impl UIObject for ChatEdit {
-    fn priority(&self) -> u32 {
-        self.priority.get()
-    }
-
-    fn init(&self) {
-        let mut guard = self.editor.lock_blocking();
-        assert!(guard.is_none());
-        *guard = Some(Editor::new(
-            self.text.clone(),
-            self.font_size.clone(),
-            self.text_color.clone(),
-            self.window_scale.clone(),
-            self.lineheight.clone(),
-        ));
-    }
-
-    async fn start(self: Arc<Self>, ex: ExecutorPtr) {
-        let me = Arc::downgrade(&self);
-
-        let node_ref = &self.node.upgrade().unwrap();
-
-        let method_sub = node_ref.subscribe_method_call("insert_text").unwrap();
-        let me2 = me.clone();
-        let insert_text_task =
-            ex.spawn(
-                async move { while Self::process_insert_text_method(&me2, &method_sub).await {} },
-            );
-
-        let method_sub = node_ref.subscribe_method_call("focus").unwrap();
-        let me2 = me.clone();
-        let focus_task =
-            ex.spawn(async move { while Self::process_focus_method(&me2, &method_sub).await {} });
-
-        let method_sub = node_ref.subscribe_method_call("unfocus").unwrap();
-        let me2 = me.clone();
-        let unfocus_task =
-            ex.spawn(async move { while Self::process_unfocus_method(&me2, &method_sub).await {} });
-
-        let mut on_modify = OnModify::new(ex.clone(), self.node.clone(), me.clone());
-        on_modify.when_change(self.is_focused.prop(), Self::change_focus);
-
-        // When text has been changed.
-        // Cursor and selection might be invalidated.
-        async fn reset(self_: Arc<ChatEdit>, batch: BatchGuardPtr) {
-            let atom = &mut batch.spawn();
-            //self_.select_text.set_null(Role::Internal, 0).unwrap();
-            self_.scroll.set(atom, 0.);
-            self_.redraw(atom).await;
-        }
-        async fn redraw(self_: Arc<ChatEdit>, batch: BatchGuardPtr) {
-            let atom = &mut batch.spawn();
-            self_.redraw(atom).await;
-        }
-        async fn set_text(self_: Arc<ChatEdit>, batch: BatchGuardPtr) {
-            self_.lock_editor().await.on_text_prop_changed().await;
-            let atom = &mut batch.spawn();
-            self_.redraw(atom).await;
-        }
-
-        on_modify.when_change(self.rect.prop(), redraw);
-        on_modify.when_change(self.baseline.prop(), redraw);
-        on_modify.when_change(self.lineheight.prop(), redraw);
-        on_modify.when_change(self.select_ascent.prop(), redraw);
-        on_modify.when_change(self.select_descent.prop(), redraw);
-        on_modify.when_change(self.handle_descent.prop(), redraw);
-        on_modify.when_change(self.padding.clone(), redraw);
-        on_modify.when_change(self.text.prop(), set_text);
-        // The commented properties are modified on input events
-        // So then redraw() will get repeatedly triggered when these properties
-        // are changed. We should find a solution. For now the hooks are disabled.
-        //on_modify.when_change(scroll.prop(), redraw);
-        //on_modify.when_change(cursor_pos.prop(), redraw);
-        on_modify.when_change(self.font_size.prop(), redraw);
-        // We must also reshape text
-        on_modify.when_change(self.text.prop(), reset);
-        on_modify.when_change(self.text_color.prop(), redraw);
-        on_modify.when_change(self.hi_bg_color.prop(), redraw);
-        //on_modify.when_change(selected.clone(), redraw);
-        on_modify.when_change(self.z_index.prop(), redraw);
-        on_modify.when_change(self.debug.prop(), redraw);
-
-        async fn regen_cursor(self_: Arc<ChatEdit>, _batch: BatchGuardPtr) {
-            // Free the cache
-            *self_.cursor_mesh.lock() = None;
-        }
-        on_modify.when_change(self.cursor_color.prop(), regen_cursor);
-        on_modify.when_change(self.cursor_ascent.prop(), regen_cursor);
-        on_modify.when_change(self.cursor_descent.prop(), regen_cursor);
-        on_modify.when_change(self.cursor_width.prop(), regen_cursor);
-
-        let me2 = me.clone();
-        let cursor_blink_time = self.cursor_blink_time.clone();
-        let cursor_idle_time = self.cursor_idle_time.clone();
-        let blinking_cursor_task = ex.spawn(async move {
-            loop {
-                msleep(cursor_blink_time.get() as u64).await;
-
-                let self_ = me2.upgrade().unwrap();
-
-                if self_.blink_is_paused.swap(false, Ordering::Relaxed) {
-                    msleep(cursor_idle_time.get() as u64).await;
-                    continue
-                }
-
-                if !self_.rect.has_cached() {
-                    continue
-                }
-
-                // Invert the bool
-                self_.cursor_is_visible.fetch_not(Ordering::Relaxed);
-                let atom = &mut self_.render_api.make_guard(gfxtag!("ChatEdit::start"));
-                self_.redraw_cursor(atom.batch_id).await;
-            }
-        });
-
-        let mut tasks = vec![insert_text_task, focus_task, unfocus_task, blinking_cursor_task];
-        tasks.append(&mut on_modify.tasks);
-
-        #[cfg(target_os = "android")]
-        {
-            let recvr = self.lock_editor().await.recvr.clone();
-            let me2 = me.clone();
-            let autosuggest_task = ex.spawn(async move {
-                loop {
-                    let Ok(ev) = recvr.recv().await else {
-                        t!("Event relayer closed");
-                        break
-                    };
-
-                    let Some(self_) = me2.upgrade() else {
-                        // Should not happen
-                        panic!("self destroyed before autosuggest_task was stopped!");
-                    };
-
-                    self_.handle_android_event(ev).await;
-                }
-            });
-            tasks.push(autosuggest_task);
-        }
-
-        *self.tasks.lock() = tasks;
-    }
-
-    fn stop(&self) {
-        self.tasks.lock().clear();
-        *self.parent_rect.lock() = None;
-        self.key_repeat.lock().clear();
-        *self.cursor_mesh.lock() = None;
-        *self.editor.lock_blocking() = None;
-    }
-
-    async fn draw(
-        &self,
-        parent_rect: Rectangle,
-        trace_id: u32,
-        atom: &mut PropertyAtomicGuard,
-    ) -> Option<DrawUpdate> {
-        t!("ChatEdit::draw({:?}, {trace_id})", self.node());
-        *self.parent_rect.lock() = Some(parent_rect);
-
-        Some(self.make_draw_calls(trace_id, atom).await)
-    }
-
-    async fn handle_char(&self, key: char, mods: KeyMods, repeat: bool) -> bool {
-        t!("handle_char({key}, {mods:?}, {repeat})");
-        // First filter for only single digit keys
-        if DISALLOWED_CHARS.contains(&key) {
-            return false
-        }
-
-        if !self.is_focused.get() {
-            return false
-        }
-
-        // Must be updated before checking the mods. You can press ctrl+a, then release ctrl
-        // before a is released. Then the repeater never gets reset, and uses any old value
-        // it has from before for a.
-        let actions = {
-            let mut repeater = self.key_repeat.lock();
-            repeater.key_down(PressedKey::Char(key), repeat)
-        };
-
-        let atom = &mut self.render_api.make_guard(gfxtag!("ChatEdit::handle_char"));
-
-        if mods.ctrl || mods.alt || mods.logo {
-            if repeat {
-                return false
-            }
-            return self.handle_shortcut(key, &mods, atom).await
-        }
-
-        // Do nothing
-        if actions == 0 {
-            return true
-        }
-
-        t!("Key {:?} has {} actions", key, actions);
-        let key_str = key.to_string().repeat(actions as usize);
-        self.insert(&key_str, atom).await;
-        self.redraw(atom).await;
-        true
-    }
-
-    async fn handle_key_down(&self, key: KeyCode, mods: KeyMods, repeat: bool) -> bool {
-        t!("handle_key_down({key:?}, {mods:?}, {repeat})");
-        // First filter for only single digit keys
-        // Avoid processing events handled by handle_char()
-        if !ALLOWED_KEYCODES.contains(&key) {
-            return false
-        }
-
-        if !self.is_focused.get() {
-            return false
-        }
-
-        let actions = {
-            let mut repeater = self.key_repeat.lock();
-            repeater.key_down(PressedKey::Key(key), repeat)
-        };
-
-        // Suppress noisy message
-        if actions > 0 {
-            t!("Key {:?} has {} actions", key, actions);
-        }
-
-        let atom = &mut self.render_api.make_guard(gfxtag!("ChatEdit::handle_key_down"));
-
-        let mut is_handled = false;
-        for _ in 0..actions {
-            if self.handle_key(&key, &mods, atom).await {
-                is_handled = true;
-            }
-        }
-        is_handled
-    }
-
-    async fn handle_mouse_btn_down(&self, btn: MouseButton, mut mouse_pos: Point) -> bool {
-        if !self.is_active.get() {
-            return false
-        }
-
-        let rect = self.rect.get();
-
-        if btn != MouseButton::Left {
-            if btn == MouseButton::Right && rect.contains(mouse_pos) {
-                if self.text.get().is_empty() {
-                    self.node().trigger("paste_request", vec![]).await.unwrap();
-                }
-                return true
-            }
-            return false
-        }
-
-        if !rect.contains(mouse_pos) {
-            return false
-        }
-
-        let atom = &mut self.render_api.make_guard(gfxtag!("ChatEdit::handle_mouse_btn_down"));
-
-        // clicking inside box will:
-        // 1. make it active
-        // 2. begin selection
-        if self.is_focused.get() {
-            d!("ChatEdit clicked");
-        } else {
-            d!("ChatEdit focused");
-            self.is_focused.set(atom, true);
-        }
-
-        // Move mouse pos within this widget
-        self.abs_to_local(&mut mouse_pos);
-
-        {
-            let mut txt_ctx = text2::TEXT_CTX.get().await;
-            let mut editor = self.lock_editor().await;
-            let mut drv = editor.driver(&mut txt_ctx).unwrap();
-            drv.move_to_point(mouse_pos.x, mouse_pos.y);
-        }
-
-        if !self.select_text.is_null(0).unwrap() {
-            self.select_text.clone().set_null(atom, Role::Internal, 0).unwrap();
-        }
-
-        self.mouse_btn_held.store(true, Ordering::Relaxed);
-
-        self.pause_blinking();
-        self.redraw(atom).await;
-        true
-    }
-
-    async fn handle_mouse_btn_up(&self, _btn: MouseButton, _mouse_pos: Point) -> bool {
-        if !self.is_active.get() {
-            return false
-        }
-
-        // releasing mouse button will end selection
-        self.mouse_btn_held.store(false, Ordering::Relaxed);
-        false
-    }
-
-    async fn handle_mouse_move(&self, mut mouse_pos: Point) -> bool {
-        if !self.is_active.get() {
-            return false
-        }
-
-        let rect = self.rect.get();
-        self.is_mouse_hover.store(rect.contains(mouse_pos), Ordering::Relaxed);
-
-        if !self.mouse_btn_held.load(Ordering::Relaxed) {
-            return false
-        }
-
-        let atom = &mut self.render_api.make_guard(gfxtag!("ChatEdit::handle_mouse_move"));
-
-        // if active and selection_active, then use x to modify the selection.
-        // also implement scrolling when cursor is to the left or right
-        // just scroll to the end
-        // also set cursor_pos too
-
-        // Move mouse pos within this widget
-        self.abs_to_local(&mut mouse_pos);
-
-        let seltext = {
-            let mut txt_ctx = text2::TEXT_CTX.get().await;
-            let mut editor = self.lock_editor().await;
-            let mut drv = editor.driver(&mut txt_ctx).unwrap();
-            drv.extend_selection_to_point(mouse_pos.x, mouse_pos.y);
-            editor.selected_text()
-        };
-        d!("Select {seltext:?} from {mouse_pos:?}");
-
-        // Will be None when drag select just started
-        if let Some(seltext) = seltext {
-            self.select_text.clone().set_str(atom, Role::Internal, 0, seltext).unwrap();
-        }
-
-        self.pause_blinking();
-        self.apply_cursor_scrolling(atom).await;
-        self.redraw_scroll(atom.batch_id).await;
-        self.redraw_cursor(atom.batch_id).await;
-        self.redraw_select(atom.batch_id).await;
-        true
-    }
-
-    async fn handle_mouse_wheel(&self, wheel_pos: Point) -> bool {
-        if !self.is_mouse_hover.load(Ordering::Relaxed) {
-            return false
-        }
-
-        let atom = &mut self.render_api.make_guard(gfxtag!("ChatEdit::handle_mouse_wheel"));
-
-        let mut scroll = self.scroll.get() - wheel_pos.y * self.scroll_speed.get();
-        scroll = scroll.clamp(0., self.max_scroll());
-        t!("handle_mouse_wheel({wheel_pos:?}) [scroll={scroll}]");
-        self.scroll.set(atom, scroll);
-        self.redraw_scroll(atom.batch_id).await;
-
-        true
-    }
-
-    async fn handle_touch(&self, phase: TouchPhase, id: u64, touch_pos: Point) -> bool {
-        if !self.is_active.get() {
-            return false
-        }
-
-        // Ignore multi-touch
-        if id != 0 {
-            return false
-        }
-
-        let atom = &mut self.render_api.make_guard(gfxtag!("ChatEdit::handle_touch"));
-
-        match phase {
-            TouchPhase::Started => self.handle_touch_start(touch_pos).await,
-            TouchPhase::Moved => self.handle_touch_move(touch_pos).await,
-            TouchPhase::Ended => self.handle_touch_end(atom, touch_pos).await,
-            TouchPhase::Cancelled => false,
-        }
-    }
-}

+ 0 - 2
bin/app/src/ui/baseedit/mod.rs → bin/app/src/ui/edit/filter.rs

@@ -18,8 +18,6 @@
 
 use miniquad::KeyCode;
 
-pub mod repeat;
-
 /// Filter these char events from being handled since we handle them
 /// using the key_up/key_down events.
 /// Avoids duplicate processing of keyboard input events.

+ 5 - 7
bin/app/src/ui/edit/mod.rs

@@ -47,17 +47,15 @@ use crate::{
     ExecutorPtr,
 };
 
-use super::{
-    baseedit::{
-        repeat::{PressedKey, PressedKeysSmoothRepeat},
-        ALLOWED_KEYCODES, DISALLOWED_CHARS,
-    },
-    DrawUpdate, OnModify, UIObject,
-};
+use super::{DrawUpdate, OnModify, UIObject};
 
+mod filter;
+use filter::{ALLOWED_KEYCODES, DISALLOWED_CHARS};
 mod behave;
 pub use behave::BaseEditType;
 use behave::{EditorBehavior, FingerScrollDir, MultiLine, SingleLine};
+mod repeat;
+use repeat::{PressedKey, PressedKeysSmoothRepeat};
 
 /// The travel threshold on long hold select before activating select.
 const HOLD_TRAVEL_THRESHOLD_SQ: f32 = 100.;

+ 0 - 0
bin/app/src/ui/baseedit/repeat.rs → bin/app/src/ui/edit/repeat.rs


+ 0 - 391
bin/app/src/ui/editbox/editable.rs

@@ -1,391 +0,0 @@
-/* This file is part of DarkFi (https://dark.fi)
- *
- * Copyright (C) 2020-2025 Dyne.org foundation
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-
-use crate::{
-    prop::{
-        PropertyBool, PropertyColor, PropertyFloat32, PropertyPtr, PropertyRect, PropertyStr,
-        PropertyUint32, Role,
-    },
-    text::{self, Glyph, GlyphPositionIter, TextShaperPtr},
-    util::enumerate_ref,
-};
-
-pub type TextPos = usize;
-pub type TextIdx = usize;
-
-/// Android composing text from autosuggest.
-/// We need this because IMEs can arbitrary set a composing region after
-/// the text has been committed.
-#[derive(Clone)]
-struct ComposingText {
-    /// Text that is being composed
-    compose_text: String,
-    /// Text that has been committed
-    commit_text: String,
-
-    region_start: usize,
-    region_end: usize,
-}
-
-impl ComposingText {
-    fn new() -> Self {
-        Self {
-            compose_text: String::new(),
-            commit_text: String::new(),
-            region_start: 0,
-            region_end: 0,
-        }
-    }
-
-    fn clear(&mut self) -> String {
-        self.region_start = 0;
-        self.region_end = 0;
-        let final_text =
-            std::mem::take(&mut self.commit_text) + &std::mem::take(&mut self.compose_text);
-        final_text
-    }
-
-    /// Set composing text.
-    fn compose(&mut self, text: String) {
-        self.compose_text = text;
-
-        self.region_start = self.commit_text.len();
-        self.region_end = self.region_start + self.compose_text.len();
-    }
-
-    /// Commit the composing text.
-    fn commit(&mut self) {
-        self.commit_text += &self.compose_text;
-        self.compose_text.clear();
-
-        self.region_start = self.commit_text.len();
-        self.region_end = self.commit_text.len();
-    }
-
-    /// Override the composing region for display.
-    /// Anyone who looks closely at this impl might thing it's wrong that subsequent
-    /// calls to compose() will ignore what's set here, but indeed this is how Android behaves.
-    fn set_compose_region(&mut self, start: usize, end: usize) {
-        assert!(start <= end);
-        assert!(end <= self.commit_text.len() + self.compose_text.len());
-        self.region_start = start;
-        self.region_end = end;
-    }
-}
-
-#[derive(Clone)]
-pub struct RenderedEditable {
-    pub glyphs: Vec<Glyph>,
-    pub under_start: TextPos,
-    pub under_end: TextPos,
-}
-
-impl RenderedEditable {
-    fn new(glyphs: Vec<Glyph>, under_start: TextPos, under_end: TextPos) -> Self {
-        let mut self_ = Self { glyphs, under_start: 0, under_end: 0 };
-        self_.under_start = self_.idx_to_pos(under_start);
-        self_.under_end = self_.idx_to_pos(under_end);
-        self_
-    }
-
-    /// Which glyph contains the char at idx?
-    fn idx_to_pos(&self, idx: TextIdx) -> TextPos {
-        let mut total = 0;
-        for (i, glyph) in enumerate_ref(&self.glyphs) {
-            total += glyph.substr.len();
-            if idx < total {
-                return i
-            }
-        }
-        return self.glyphs.len()
-    }
-
-    /// Converts glyph pos to idx in the string
-    pub fn pos_to_idx(&self, pos: TextPos) -> TextIdx {
-        let mut idx = 0;
-        for (i, glyph) in enumerate_ref(&self.glyphs) {
-            if i == pos {
-                return idx
-            }
-            idx += glyph.substr.len();
-        }
-        return idx
-    }
-
-    pub fn has_underline(&self) -> bool {
-        self.under_start != self.under_end
-    }
-
-    /// Converts x offset to glyph pos. Will round to the closest side.
-    pub fn x_to_pos(&self, x: f32, font_size: f32, window_scale: f32, baseline: f32) -> TextPos {
-        debug!(target: "ui::editbox", "x_to_pos({x})");
-        let glyph_pos_iter =
-            GlyphPositionIter::new(font_size, window_scale, &self.glyphs, baseline);
-        for (glyph_idx, glyph_rect) in glyph_pos_iter.enumerate() {
-            if x >= glyph_rect.rhs() {
-                continue
-            }
-
-            let midpoint = glyph_rect.x + glyph_rect.w / 2.;
-            if x < midpoint {
-                return glyph_idx
-            }
-            assert!(x >= midpoint);
-            return glyph_idx + 1
-        }
-        // Everything to the right is at the end
-        self.glyphs.len()
-    }
-
-    pub fn pos_to_xw(
-        &self,
-        pos: TextPos,
-        font_size: f32,
-        window_scale: f32,
-        baseline: f32,
-    ) -> (f32, f32) {
-        debug!(target: "ui::editbox", "pos_to_xw({pos}) [glyphs_len={}]", self.glyphs.len());
-        let mut glyph_pos_iter =
-            GlyphPositionIter::new(font_size, window_scale, &self.glyphs, baseline);
-        let mut end = 0.;
-        for (glyph_idx, glyph_rect) in glyph_pos_iter.enumerate() {
-            if glyph_idx == pos {
-                return (glyph_rect.x, glyph_rect.w)
-            }
-            end = glyph_rect.rhs();
-        }
-        (end, 0.)
-    }
-}
-
-/// Represents a string with a cursor. The cursor can be moved in terms of glyphs, which does
-/// not always correspond to chars in the string. We refer to byte indexes as idx, and glyph
-/// indexes as pos.
-///
-/// The full string is: `before_text + commit_text + compose_text + after_text`.
-/// The commit/compose text is the current text being composed which is needed for Android
-/// autosuggest input.
-///
-/// Before and after text refers to the cursor. The cursor position is always everything
-/// before `after_text`, that is: `before_text + commit_text + compose_text`.
-///
-/// We have to be careful when adjusting the cursor and other related ops like selecting text
-/// to first render and move in terms of glyphs due to kerning. For example the chars "ae"
-/// may be rendered as a single glyph in some fonts. Same for emojis represented by multiple
-/// chars which are often not even a single byte.
-pub struct Editable {
-    text_shaper: TextShaperPtr,
-
-    composer: ComposingText,
-
-    before_text: String,
-    after_text: String,
-
-    font_size: PropertyFloat32,
-    window_scale: PropertyFloat32,
-    baseline: PropertyFloat32,
-}
-
-impl Editable {
-    pub fn new(
-        text_shaper: TextShaperPtr,
-
-        font_size: PropertyFloat32,
-        window_scale: PropertyFloat32,
-        baseline: PropertyFloat32,
-    ) -> Self {
-        Self {
-            text_shaper,
-            composer: ComposingText::new(),
-            before_text: String::new(),
-            after_text: String::new(),
-            font_size,
-            window_scale,
-            baseline,
-        }
-    }
-
-    // reset composition
-    // set text
-    // find pos
-    // compose
-    // commit
-    // set_compose_region
-    // delete (forward, back)
-    // set cursor
-
-    /// Reset any composition in progress
-    pub fn end_compose(&mut self) {
-        //#[cfg(target_os = "android")]
-        //crate::android::cancel_composition();
-
-        //debug!(target: "ui::editbox", "end_compose() [editable={self:?}]");
-        let final_text = self.composer.clear();
-        self.before_text += &final_text;
-    }
-
-    pub fn get_text_before(&self) -> String {
-        let text =
-            self.before_text.clone() + &self.composer.commit_text + &self.composer.compose_text;
-        text
-    }
-    pub fn get_text(&self) -> String {
-        let text = self.get_text_before() + &self.after_text;
-        text
-    }
-
-    pub fn set_text(&mut self, before: String, after: String) {
-        self.before_text = before;
-        self.after_text = after;
-    }
-
-    pub fn compose(&mut self, suggest_text: &str, is_commit: bool) {
-        //composer.activate_or_cont(self.cursor_pos.get() as usize);
-        self.composer.compose(suggest_text.to_string());
-        if is_commit {
-            self.composer.commit();
-        }
-    }
-    /// Convenience function
-    pub fn set_compose_region(&mut self, start: usize, end: usize) {
-        self.composer.set_compose_region(start, end);
-    }
-
-    pub fn delete(&mut self, before: usize, after: usize) {
-        self.end_compose();
-
-        let mut chars = self.before_text.chars();
-        // nightly feature, commenting it for now
-        //chars.advance_back_by(before);
-        self.before_text = chars.as_str().to_string();
-
-        let mut chars = self.after_text.chars();
-        // nightly feature, commenting it for now
-        //chars.advance_by(after);
-        self.after_text = chars.as_str().to_string();
-    }
-
-    /// Move the cursor. This offset should be computed from the glyphs.
-    pub fn move_cursor(&mut self, dir: isize) {
-        self.end_compose();
-
-        let rendered = self.render();
-        let mut cursor_pos = self.get_cursor_pos(&rendered);
-
-        // Move the cursor pos
-        if dir < 0 {
-            assert!(-dir >= 0);
-            let dir = -dir as usize;
-            if cursor_pos > 0 {
-                cursor_pos -= dir;
-            }
-        } else {
-            assert!(dir >= 0);
-            cursor_pos += dir as usize;
-            let glyphs_len = rendered.glyphs.len();
-            if cursor_pos > glyphs_len {
-                cursor_pos = glyphs_len;
-            }
-        }
-
-        // Convert cursor pos to string idx
-        let idx = rendered.pos_to_idx(cursor_pos);
-        self.set_cursor_idx(idx);
-    }
-
-    pub fn set_cursor_idx(&mut self, idx: TextPos) {
-        // move_cursor() also calls this, but should be fine.
-        self.end_compose();
-
-        let mut text = self.get_text();
-        let after_text = text.split_off(idx);
-        self.before_text = text;
-        self.after_text = after_text;
-    }
-
-    pub fn move_start(&mut self) {
-        self.end_compose();
-        self.after_text = self.get_text();
-        self.before_text.clear();
-    }
-    pub fn move_end(&mut self) {
-        self.end_compose();
-        self.before_text = self.get_text();
-        self.after_text.clear();
-    }
-
-    pub fn get_cursor_pos(&self, rendered: &RenderedEditable) -> TextPos {
-        let cursor_off = self.get_text_before().len();
-        let cursor_pos = rendered.idx_to_pos(cursor_off);
-        cursor_pos
-    }
-
-    pub fn render(&self) -> RenderedEditable {
-        let font_size = self.font_size.get();
-        let window_scale = self.window_scale.get();
-
-        let text = self.get_text();
-        let glyphs = self.text_shaper.shape(text, font_size, window_scale);
-
-        let compose_off = self.before_text.len();
-        RenderedEditable::new(
-            glyphs,
-            compose_off + self.composer.region_start,
-            compose_off + self.composer.region_end,
-        )
-    }
-}
-
-impl std::fmt::Debug for Editable {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(
-            f,
-            "({}, {}, {}, {})",
-            self.before_text,
-            self.composer.commit_text,
-            self.composer.compose_text,
-            self.after_text
-        )
-    }
-}
-
-fn glyphs_to_string(glyphs: &Vec<Glyph>) -> String {
-    let mut text = String::new();
-    for (i, glyph) in glyphs.iter().enumerate() {
-        text.push_str(&glyph.substr);
-    }
-    text
-}
-
-#[derive(Clone)]
-pub struct Selection {
-    pub start: TextPos,
-    pub end: TextPos,
-}
-
-impl Selection {
-    pub fn new(start: TextPos, end: TextPos) -> Self {
-        Self { start, end }
-    }
-}
-
-impl std::fmt::Debug for Selection {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "[{}, {}]", self.start, self.end)
-    }
-}

+ 0 - 1633
bin/app/src/ui/editbox/mod.rs

@@ -1,1633 +0,0 @@
-/* This file is part of DarkFi (https://dark.fi)
- *
- * Copyright (C) 2020-2025 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 async_lock::Mutex as AsyncMutex;
-use async_trait::async_trait;
-use atomic_float::AtomicF32;
-use darkfi::system::msleep;
-use miniquad::{window, KeyCode, KeyMods, MouseButton, TouchPhase};
-use rand::{rngs::OsRng, Rng};
-use std::{
-    collections::HashMap,
-    sync::{
-        atomic::{AtomicBool, Ordering},
-        Arc, Mutex as SyncMutex, OnceLock, Weak,
-    },
-    time::Instant,
-};
-
-use crate::{
-    error::Result,
-    gfx::{
-        gfxtag, DrawCall, DrawInstruction, DrawMesh, GraphicsEventPublisherPtr, Point, Rectangle,
-        RenderApi, TextureId, Vertex,
-    },
-    mesh::{MeshBuilder, MeshInfo, COLOR_BLUE, COLOR_WHITE},
-    prop::{
-        BatchGuardPtr, PropertyAtomicGuard, PropertyBool, PropertyColor, PropertyFloat32,
-        PropertyPtr, PropertyRect, PropertyStr, PropertyUint32, Role,
-    },
-    pubsub::Subscription,
-    scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
-    text::{self, Glyph, GlyphPositionIter, TextShaperPtr},
-    util::{enumerate_ref, unixtime, zip3},
-    ExecutorPtr,
-};
-
-use super::{DrawUpdate, OnModify, UIObject};
-
-pub mod editable;
-use editable::{Editable, Selection, TextIdx, TextPos};
-
-pub mod repeat;
-use repeat::{PressedKey, PressedKeysSmoothRepeat};
-
-macro_rules! d { ($($arg:tt)*) => { debug!(target: "ui::editbox", $($arg)*); } }
-macro_rules! t { ($($arg:tt)*) => { trace!(target: "ui::editbox", $($arg)*); } }
-
-// EOL whitespace is given a nudge since it has a width of 0 after text shaping
-const CURSOR_EOL_WS_NUDGE: f32 = 0.8;
-// EOL chars are more aesthetic when given a smallish nudge
-const CURSOR_EOL_NUDGE: f32 = 0.2;
-
-pub fn eol_nudge(font_size: f32, glyphs: &Vec<Glyph>) -> f32 {
-    (font_size * CURSOR_EOL_NUDGE).round()
-}
-
-#[derive(Clone)]
-struct TouchStartInfo {
-    pos: Point,
-    instant: std::time::Instant,
-}
-
-impl TouchStartInfo {
-    fn new(pos: Point) -> Self {
-        Self { pos, instant: std::time::Instant::now() }
-    }
-}
-
-#[derive(Clone)]
-enum TouchStateAction {
-    Inactive,
-    Started { pos: Point, instant: std::time::Instant },
-    StartSelect,
-    Select,
-    DragSelectHandle { side: isize },
-    ScrollHoriz { start_pos: Point, scroll_start: f32 },
-}
-
-struct TouchInfo {
-    state: TouchStateAction,
-    start: Option<TouchStartInfo>,
-
-    scroll: PropertyFloat32,
-}
-
-impl TouchInfo {
-    fn new(scroll: PropertyFloat32) -> Self {
-        Self { state: TouchStateAction::Inactive, start: None, scroll }
-    }
-
-    fn start(&mut self, pos: Point) {
-        d!("TouchStateAction::Started");
-        self.state = TouchStateAction::Started { pos, instant: std::time::Instant::now() };
-    }
-
-    fn stop(&mut self) -> TouchStateAction {
-        std::mem::replace(&mut self.state, TouchStateAction::Inactive)
-    }
-
-    fn update(&mut self, pos: &Point) {
-        match &self.state {
-            TouchStateAction::Started { pos: start_pos, instant } => {
-                let travel_dist = pos.dist_sq(*start_pos);
-                let x_dist = pos.x - start_pos.x;
-                let elapsed = instant.elapsed().as_millis();
-
-                if travel_dist < 5. {
-                    if elapsed > 1000 {
-                        d!("TouchStateAction::StartSelect");
-                        self.state = TouchStateAction::StartSelect;
-                    }
-                } else if x_dist.abs() > 5. {
-                    d!("TouchStateAction::ScrollHoriz");
-                    let scroll_start = self.scroll.get();
-                    self.state =
-                        TouchStateAction::ScrollHoriz { start_pos: *start_pos, scroll_start };
-                }
-            }
-            _ => {}
-        }
-    }
-}
-
-pub type EditBoxPtr = Arc<EditBox>;
-
-pub struct EditBox {
-    node: SceneNodeWeak,
-    tasks: OnceLock<Vec<smol::Task<()>>>,
-    render_api: RenderApi,
-    text_shaper: TextShaperPtr,
-    key_repeat: SyncMutex<PressedKeysSmoothRepeat>,
-
-    glyphs: SyncMutex<Vec<Glyph>>,
-    /// DC key for the text
-    text_dc_key: u64,
-    cursor_mesh: SyncMutex<Option<DrawMesh>>,
-    /// DC key for the cursor. Allows updating cursor independently.
-    cursor_dc_key: u64,
-
-    is_active: PropertyBool,
-    is_focused: PropertyBool,
-    rect: PropertyRect,
-    baseline: PropertyFloat32,
-    scroll: PropertyFloat32,
-    scroll_speed: PropertyFloat32,
-    cursor_pos: PropertyUint32,
-    font_size: PropertyFloat32,
-    text: PropertyStr,
-    text_color: PropertyColor,
-    text_hi_color: PropertyColor,
-    cursor_color: PropertyColor,
-    cursor_width: PropertyFloat32,
-    cursor_ascent: PropertyFloat32,
-    cursor_descent: PropertyFloat32,
-    cursor_blink_time: PropertyUint32,
-    cursor_idle_time: PropertyUint32,
-    hi_bg_color: PropertyColor,
-    select_ascent: PropertyFloat32,
-    select_descent: PropertyFloat32,
-    selected: PropertyPtr,
-    z_index: PropertyUint32,
-    priority: PropertyUint32,
-    debug: PropertyBool,
-
-    editable: SyncMutex<Editable>,
-    select: SyncMutex<Vec<Selection>>,
-
-    mouse_btn_held: AtomicBool,
-    cursor_is_visible: AtomicBool,
-    blink_is_paused: AtomicBool,
-    /// Used to explicitly hide the cursor. Must be manually re-enabled.
-    hide_cursor: AtomicBool,
-
-    touch_info: SyncMutex<TouchInfo>,
-    is_phone_select: AtomicBool,
-
-    old_window_scale: AtomicF32,
-    window_scale: PropertyFloat32,
-    parent_rect: SyncMutex<Option<Rectangle>>,
-    is_mouse_hover: AtomicBool,
-}
-
-impl EditBox {
-    pub async fn new(
-        node: SceneNodeWeak,
-        window_scale: PropertyFloat32,
-        render_api: RenderApi,
-        text_shaper: TextShaperPtr,
-    ) -> Pimpl {
-        t!("EditBox::new()");
-
-        let node_ref = &node.upgrade().unwrap();
-        let is_active = PropertyBool::wrap(node_ref, Role::Internal, "is_active", 0).unwrap();
-        let is_focused = PropertyBool::wrap(node_ref, Role::Internal, "is_focused", 0).unwrap();
-        let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
-        let baseline = PropertyFloat32::wrap(node_ref, Role::Internal, "baseline", 0).unwrap();
-        let scroll = PropertyFloat32::wrap(node_ref, Role::Internal, "scroll", 0).unwrap();
-        let scroll_speed =
-            PropertyFloat32::wrap(node_ref, Role::Internal, "scroll_speed", 0).unwrap();
-        let cursor_pos = PropertyUint32::wrap(node_ref, Role::Internal, "cursor_pos", 0).unwrap();
-        let font_size = PropertyFloat32::wrap(node_ref, Role::Internal, "font_size", 0).unwrap();
-        let text = PropertyStr::wrap(node_ref, Role::Internal, "text", 0).unwrap();
-        let text_color = PropertyColor::wrap(node_ref, Role::Internal, "text_color").unwrap();
-        let text_hi_color = PropertyColor::wrap(node_ref, Role::Internal, "text_hi_color").unwrap();
-        let cursor_color = PropertyColor::wrap(node_ref, Role::Internal, "cursor_color").unwrap();
-        let cursor_width =
-            PropertyFloat32::wrap(node_ref, Role::Internal, "cursor_width", 0).unwrap();
-        let cursor_ascent =
-            PropertyFloat32::wrap(node_ref, Role::Internal, "cursor_ascent", 0).unwrap();
-        let cursor_descent =
-            PropertyFloat32::wrap(node_ref, Role::Internal, "cursor_descent", 0).unwrap();
-        let hi_bg_color = PropertyColor::wrap(node_ref, Role::Internal, "hi_bg_color").unwrap();
-        let select_ascent =
-            PropertyFloat32::wrap(node_ref, Role::Internal, "select_ascent", 0).unwrap();
-        let select_descent =
-            PropertyFloat32::wrap(node_ref, Role::Internal, "select_descent", 0).unwrap();
-        let selected = node_ref.get_property("selected").unwrap();
-        let cursor_blink_time =
-            PropertyUint32::wrap(node_ref, Role::Internal, "cursor_blink_time", 0).unwrap();
-        let cursor_idle_time =
-            PropertyUint32::wrap(node_ref, Role::Internal, "cursor_idle_time", 0).unwrap();
-        let z_index = PropertyUint32::wrap(node_ref, Role::Internal, "z_index", 0).unwrap();
-        let priority = PropertyUint32::wrap(node_ref, Role::Internal, "priority", 0).unwrap();
-        let debug = PropertyBool::wrap(node_ref, Role::Internal, "debug", 0).unwrap();
-
-        let node_name = node_ref.name.clone();
-        let node_id = node_ref.id;
-
-        // Must do this whenever the text changes
-        let glyphs = text_shaper.shape(text.get(), font_size.get(), window_scale.get());
-
-        let self_ = Arc::new(Self {
-            node,
-            tasks: OnceLock::new(),
-            render_api,
-            text_shaper: text_shaper.clone(),
-            key_repeat: SyncMutex::new(PressedKeysSmoothRepeat::new(400, 50)),
-
-            glyphs: SyncMutex::new(glyphs),
-            text_dc_key: OsRng.gen(),
-            cursor_mesh: SyncMutex::new(None),
-            cursor_dc_key: OsRng.gen(),
-
-            is_active,
-            is_focused,
-            rect,
-            baseline: baseline.clone(),
-            scroll: scroll.clone(),
-            scroll_speed,
-            cursor_pos,
-            font_size: font_size.clone(),
-            text,
-            text_color,
-            text_hi_color,
-            cursor_color,
-            cursor_width,
-            cursor_ascent,
-            cursor_descent,
-            cursor_blink_time,
-            cursor_idle_time,
-            hi_bg_color,
-            select_ascent,
-            select_descent,
-            selected,
-            z_index,
-            priority,
-            debug,
-
-            editable: SyncMutex::new(Editable::new(
-                text_shaper,
-                font_size,
-                window_scale.clone(),
-                baseline,
-            )),
-            select: SyncMutex::new(vec![]),
-
-            mouse_btn_held: AtomicBool::new(false),
-            cursor_is_visible: AtomicBool::new(true),
-            blink_is_paused: AtomicBool::new(false),
-            hide_cursor: AtomicBool::new(false),
-
-            touch_info: SyncMutex::new(TouchInfo::new(scroll)),
-            is_phone_select: AtomicBool::new(false),
-
-            old_window_scale: AtomicF32::new(window_scale.get()),
-            window_scale,
-            parent_rect: SyncMutex::new(None),
-            is_mouse_hover: AtomicBool::new(false),
-        });
-
-        Pimpl::EditBox(self_)
-    }
-
-    fn node(&self) -> SceneNodePtr {
-        self.node.upgrade().unwrap()
-    }
-
-    /// Called whenever the text or any text property changes.
-    fn regen_text_mesh(&self, mut clip: Rectangle) -> DrawMesh {
-        clip.x = 0.;
-        clip.y = 0.;
-
-        let is_focused = self.is_focused.get();
-        let text = self.text.get();
-        let font_size = self.font_size.get();
-        let window_scale = self.window_scale.get();
-        let text_color = self.text_color.get();
-        let text_hi_color = self.text_hi_color.get();
-        let baseline = self.baseline.get();
-        let scroll = self.scroll.get();
-        let cursor_pos = self.cursor_pos.get() as usize;
-        let cursor_color = self.cursor_color.get();
-        let debug = self.debug.get();
-        //debug!(target: "ui::editbox", "Rendering text '{text}' clip={clip:?}");
-        //debug!(target: "ui::editbox", "    cursor_pos={cursor_pos}, is_focused={is_focused}");
-
-        let rendered = self.editable.lock().unwrap().render();
-        let atlas =
-            text::make_texture_atlas(&self.render_api, gfxtag!("editbox_txt"), &rendered.glyphs);
-
-        let mut mesh = MeshBuilder::new(gfxtag!("editbox_txt"));
-
-        let selections = self.select.lock().unwrap().clone();
-        // Just an assert
-        if self.is_phone_select.load(Ordering::Relaxed) {
-            assert!(selections.len() <= 1);
-        }
-        for select in &selections {
-            self.draw_selected(&mut mesh, select, &rendered.glyphs, clip.h);
-        }
-        let select_marks = self.mark_selected_glyphs(&rendered.glyphs, selections);
-
-        if rendered.has_underline() {
-            self.draw_underline(
-                &mut mesh,
-                &rendered.glyphs,
-                clip.h,
-                rendered.under_start,
-                rendered.under_end,
-            );
-        }
-
-        let glyph_pos_iter =
-            GlyphPositionIter::new(font_size, window_scale, &rendered.glyphs, baseline);
-
-        for (glyph_idx, mut glyph_rect, glyph, is_selected) in
-            zip3(glyph_pos_iter, rendered.glyphs.iter(), select_marks.iter())
-        {
-            let uv_rect = atlas.fetch_uv(glyph.glyph_id).expect("missing glyph UV rect");
-
-            glyph_rect.x -= scroll;
-
-            //mesh.draw_outline(&glyph_rect, COLOR_BLUE, 2.);
-            let mut color = text_color.clone();
-            if *is_selected {
-                color = text_hi_color.clone();
-            }
-            if glyph.sprite.has_color {
-                color = COLOR_WHITE;
-            }
-            mesh.draw_box(&glyph_rect, color, uv_rect);
-        }
-
-        if debug {
-            mesh.draw_outline(&clip, COLOR_BLUE, 1.);
-        }
-
-        mesh.alloc(&self.render_api).draw_with_texture(atlas.texture)
-    }
-
-    fn regen_cursor_mesh(&self) -> DrawMesh {
-        let cursor_width = self.cursor_width.get();
-        let cursor_ascent = self.cursor_ascent.get();
-        let cursor_descent = self.cursor_descent.get();
-        let baseline = self.baseline.get();
-
-        let cursor_rect = Rectangle {
-            x: 0.,
-            y: baseline - cursor_ascent,
-            w: cursor_width,
-            h: cursor_ascent + cursor_descent,
-        };
-        let cursor_color = self.cursor_color.get();
-
-        let mut mesh = MeshBuilder::new(gfxtag!("editbox_cursor"));
-        mesh.draw_filled_box(&cursor_rect, cursor_color);
-        mesh.alloc(&self.render_api).draw_untextured()
-    }
-
-    fn cursor_px_offset(&self) -> f32 {
-        assert!(self.is_focused.get());
-
-        let (cursor_pos, glyphs) = {
-            let editable = self.editable.lock().unwrap();
-            let rendered = editable.render();
-            let cursor_pos = editable.get_cursor_pos(&rendered);
-            (cursor_pos, rendered.glyphs)
-        };
-
-        let font_size = self.font_size.get();
-        let window_scale = self.window_scale.get();
-        let baseline = self.baseline.get();
-        let scroll = self.scroll.get();
-        // Add composer glyphs too
-        let glyph_pos_iter = GlyphPositionIter::new(font_size, window_scale, &glyphs, baseline);
-        // Used for drawing the cursor when it's at the end of the line.
-        let mut rhs = 0.;
-
-        if cursor_pos == 0 {
-            return 0.
-        }
-
-        for (glyph_idx, (mut glyph_rect, glyph)) in glyph_pos_iter.zip(glyphs.iter()).enumerate() {
-            glyph_rect.x -= scroll;
-
-            if cursor_pos == glyph_idx {
-                return glyph_rect.x
-            }
-
-            rhs = glyph_rect.rhs();
-        }
-
-        assert!(cursor_pos == glyphs.len());
-
-        rhs += eol_nudge(font_size, &glyphs);
-        rhs
-    }
-
-    fn mark_selected_glyphs(&self, glyphs: &Vec<Glyph>, selections: Vec<Selection>) -> Vec<bool> {
-        let mut marks: Vec<_> = glyphs.iter().map(|_| false).collect();
-        for select in selections {
-            let start = std::cmp::min(select.start, select.end);
-            let end = std::cmp::max(select.start, select.end);
-
-            for i in start..end {
-                marks[i] = true;
-            }
-        }
-        marks
-    }
-
-    fn draw_selected(
-        &self,
-        mesh: &mut MeshBuilder,
-        select: &Selection,
-        glyphs: &Vec<Glyph>,
-        clip_h: f32,
-    ) {
-        if select.start == select.end {
-            return
-        }
-        let start = std::cmp::min(select.start, select.end);
-        let end = std::cmp::max(select.start, select.end);
-
-        let font_size = self.font_size.get();
-        let window_scale = self.window_scale.get();
-        let baseline = self.baseline.get();
-        let scroll = self.scroll.get();
-        let hi_bg_color = self.hi_bg_color.get();
-        let glyph_pos_iter = GlyphPositionIter::new(font_size, window_scale, &glyphs, baseline);
-
-        let is_phone_select = self.is_phone_select.load(Ordering::Relaxed);
-
-        let mut start_x = 0.;
-        let mut end_x = 0.;
-        // When cursor lands at the end of the line
-        let mut rhs = 0.;
-
-        for (glyph_idx, mut glyph_rect) in glyph_pos_iter.enumerate() {
-            glyph_rect.x -= scroll;
-
-            if glyph_idx == start {
-                start_x = glyph_rect.x;
-            }
-            if glyph_idx == end {
-                end_x = glyph_rect.x;
-            }
-
-            rhs = glyph_rect.rhs();
-        }
-
-        if start == 0 {
-            start_x = scroll;
-        }
-
-        if end == glyphs.len() {
-            rhs += eol_nudge(font_size, &glyphs);
-            end_x = rhs;
-        }
-
-        let select_ascent = self.select_ascent.get();
-        let select_descent = self.select_descent.get();
-
-        let select_rect = Rectangle {
-            x: start_x,
-            y: baseline - select_ascent,
-            w: end_x - start_x,
-            h: select_ascent + select_descent,
-        };
-        mesh.draw_box(&select_rect, hi_bg_color, &Rectangle::zero());
-
-        if is_phone_select {
-            self.draw_phone_select_handle(mesh, start_x, -1.);
-            self.draw_phone_select_handle(mesh, end_x, 1.);
-        }
-    }
-
-    fn draw_phone_select_handle(&self, mesh: &mut MeshBuilder, x: f32, side: f32) {
-        t!("draw_phone_select_handle(..., {x}, {side})");
-
-        let baseline = self.baseline.get();
-        let select_ascent = self.select_ascent.get();
-        let select_descent = self.select_descent.get();
-        let color = self.text_hi_color.get();
-        // Transparent for fade
-        let mut color_trans = color.clone();
-        color_trans[3] = 0.;
-
-        // Vertical line downwards. We use this instead of draw_box() so we have a fade.
-        let verts = vec![
-            Vertex {
-                pos: [x - side * 1., baseline - select_ascent],
-                color: color_trans,
-                uv: [0., 0.],
-            },
-            Vertex {
-                pos: [x + side * 4., baseline - select_ascent],
-                color: color_trans,
-                uv: [0., 0.],
-            },
-            Vertex { pos: [x - side * 1., baseline + select_descent + 5.], color, uv: [0., 0.] },
-            Vertex { pos: [x + side * 4., baseline + select_descent + 5.], color, uv: [0., 0.] },
-        ];
-        let indices = vec![0, 2, 1, 1, 2, 3];
-        mesh.append(verts, indices);
-
-        let y = baseline + select_descent;
-
-        // The arrow itself.
-        // Go anti-clockwise
-        let verts = vec![
-            Vertex { pos: [x, y], color, uv: [0., 0.] },
-            Vertex { pos: [x, y + 50.], color, uv: [0., 0.] },
-            Vertex { pos: [x + side * 30., y + 50.], color, uv: [0., 0.] },
-            Vertex { pos: [x + side * 50., y + 25.], color, uv: [0., 0.] },
-        ];
-        let indices = vec![0, 1, 2, 0, 2, 3];
-        mesh.append(verts, indices);
-    }
-
-    fn draw_underline(
-        &self,
-        mesh: &mut MeshBuilder,
-        glyphs: &Vec<Glyph>,
-        clip_h: f32,
-        under_start: usize,
-        under_end: usize,
-    ) {
-        assert!(under_start < under_end);
-
-        let font_size = self.font_size.get();
-        let window_scale = self.window_scale.get();
-        let baseline = self.baseline.get();
-        let scroll = self.scroll.get();
-        let text_color = self.text_color.get();
-        let glyph_pos_iter = GlyphPositionIter::new(font_size, window_scale, &glyphs, baseline);
-
-        let mut start_x = 0.;
-        let mut end_x = 0.;
-        // When cursor lands at the end of the line
-        let mut rhs = 0.;
-
-        for (glyph_idx, mut glyph_rect) in glyph_pos_iter.enumerate() {
-            glyph_rect.x -= scroll;
-
-            if glyph_idx == under_start {
-                start_x = glyph_rect.x;
-            }
-            if glyph_idx == under_end {
-                end_x = glyph_rect.x;
-            }
-
-            rhs = glyph_rect.rhs();
-        }
-
-        if under_start == 0 {
-            start_x = scroll;
-        }
-
-        if under_end == glyphs.len() {
-            end_x = rhs;
-        }
-
-        // We don't need to do manual clipping since MeshBuilder should do that
-        let underline_rect = Rectangle { x: start_x, y: baseline + 6., w: end_x - start_x, h: 4. };
-        mesh.draw_box(&underline_rect, text_color, &Rectangle::zero());
-    }
-
-    async fn change_focus(self: Arc<Self>) {
-        if !self.is_active.get() {
-            return
-        }
-        d!("Focus changed");
-
-        // Cursor visibility will change so just redraw everything lol
-        self.redraw().await;
-    }
-
-    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::none();
-        self.text.set(atom, text);
-
-        self.pause_blinking();
-        self.redraw().await;
-    }
-
-    async fn handle_shortcut(&self, key: char, mods: &KeyMods, atom: &mut PropertyAtomicGuard) {
-        t!("handle_shortcut({:?}, {:?})", key, mods);
-
-        match key {
-            'c' => {
-                if mods.ctrl {
-                    self.copy_highlighted().unwrap();
-                }
-            }
-            'v' => {
-                if mods.ctrl {
-                    if let Some(text) = window::clipboard_get() {
-                        self.paste_text(text, atom).await;
-                    }
-                }
-            }
-            _ => {}
-        }
-    }
-
-    async fn handle_key(&self, key: &KeyCode, mods: &KeyMods, atom: &mut PropertyAtomicGuard) {
-        t!("handle_key({:?}, {:?})", key, mods);
-        match key {
-            KeyCode::Left => {
-                self.adjust_cursor(mods.shift, |editable| editable.move_cursor(-1));
-                self.pause_blinking();
-                //self.apply_cursor_scrolling();
-                self.redraw().await;
-            }
-            KeyCode::Right => {
-                self.adjust_cursor(mods.shift, |editable| editable.move_cursor(1));
-                self.pause_blinking();
-                //self.apply_cursor_scrolling();
-                self.redraw().await;
-            }
-            KeyCode::Kp0 => self.insert_char('0').await,
-            KeyCode::Kp1 => self.insert_char('1').await,
-            KeyCode::Kp2 => self.insert_char('2').await,
-            KeyCode::Kp3 => self.insert_char('3').await,
-            KeyCode::Kp4 => self.insert_char('4').await,
-            KeyCode::Kp5 => self.insert_char('5').await,
-            KeyCode::Kp6 => self.insert_char('6').await,
-            KeyCode::Kp7 => self.insert_char('7').await,
-            KeyCode::Kp8 => self.insert_char('8').await,
-            KeyCode::Kp9 => self.insert_char('9').await,
-            KeyCode::KpDecimal => self.insert_char('.').await,
-            KeyCode::Enter | KeyCode::KpEnter => {
-                let node = self.node.upgrade().unwrap();
-                node.trigger("enter_pressed", vec![]).await.unwrap();
-            }
-            KeyCode::Delete => {
-                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 => {
-                self.adjust_cursor(mods.shift, |editable| editable.move_start());
-                self.pause_blinking();
-                //self.apply_cursor_scrolling();
-                self.redraw().await;
-            }
-            KeyCode::End => {
-                self.adjust_cursor(mods.shift, |editable| editable.move_end());
-                self.pause_blinking();
-                //self.apply_cursor_scrolling();
-                self.redraw().await;
-            }
-            _ => {}
-        }
-    }
-
-    fn delete(&self, before: usize, after: usize) {
-        let mut editable = self.editable.lock().unwrap();
-        let selection = std::mem::take(&mut *self.select.lock().unwrap());
-
-        if selection.is_empty() {
-            editable.delete(before, after);
-            return
-        }
-
-        let sel = selection.first().unwrap();
-        let cursor_pos = std::cmp::min(sel.start, sel.end);
-
-        let render = editable.render();
-        // Same code in chatedit's TextWrap::delete_selected()
-        let mut before_text = String::new();
-        let mut after_text = String::new();
-        'next: for (pos, glyph) in render.glyphs.iter().enumerate() {
-            for select in &selection {
-                let start = std::cmp::min(select.start, select.end);
-                let end = std::cmp::max(select.start, select.end);
-
-                if start <= pos && pos < end {
-                    continue 'next
-                }
-            }
-            if pos <= cursor_pos {
-                before_text += &glyph.substr;
-            } else {
-                after_text += &glyph.substr;
-            }
-        }
-
-        editable.set_text(before_text, after_text);
-
-        self.is_phone_select.store(false, Ordering::Relaxed);
-        // Reshow cursor (if hidden)
-        self.hide_cursor.store(false, Ordering::Relaxed);
-    }
-
-    fn adjust_cursor(&self, has_shift: bool, move_cursor: impl Fn(&mut Editable)) {
-        let mut editable = self.editable.lock().unwrap();
-        let rendered = editable.render();
-        let prev_cursor_pos = editable.get_cursor_pos(&rendered);
-        move_cursor(&mut editable);
-        let cursor_pos = editable.get_cursor_pos(&rendered);
-        drop(editable);
-
-        let mut select = self.select.lock().unwrap();
-
-        // Start selection if shift is held
-        if has_shift {
-            // Create a new selection
-            if select.is_empty() {
-                select.push(Selection::new(prev_cursor_pos, cursor_pos));
-            }
-
-            // Update the selection
-            select.last_mut().unwrap().end = cursor_pos;
-        } else {
-            select.clear();
-        }
-    }
-
-    /// This will select the entire word rather than move the cursor to that location
-    fn start_touch_select(&self, x: f32) {
-        let rect = self.rect.get();
-
-        let font_size = self.font_size.get();
-        let window_scale = self.window_scale.get();
-        let baseline = self.baseline.get();
-
-        {
-            let mut editable = self.editable.lock().unwrap();
-            editable.end_compose();
-
-            let rendered = editable.render();
-            let x = x - rect.x + self.scroll.get();
-
-            let cpos = rendered.x_to_pos(x, font_size, window_scale, baseline);
-
-            // Find word start
-            let mut cpos_start = cpos;
-            while cpos_start > 0 {
-                // Is the glyph before this pos just whitespace?
-                let glyph_str = &rendered.glyphs[cpos_start - 1].substr;
-                cpos_start -= 1;
-            }
-            // Find word end
-            let mut cpos_end = cpos;
-            while cpos_end < rendered.glyphs.len() {
-                cpos_end += 1;
-                let glyph_str = &rendered.glyphs[cpos_end].substr;
-            }
-
-            let cidx_start = rendered.pos_to_idx(cpos_start);
-            let cidx_end = rendered.pos_to_idx(cpos_end);
-
-            // begin selection
-            let mut select = self.select.lock().unwrap();
-            select.clear();
-            select.push(Selection::new(cpos_start, cpos_end));
-        }
-
-        self.is_phone_select.store(true, Ordering::Relaxed);
-        // redraw() will now hide the cursor
-        self.hide_cursor.store(true, Ordering::Relaxed);
-    }
-
-    fn glyphs_to_string(glyphs: &Vec<Glyph>) -> String {
-        let mut text = String::new();
-        for (i, glyph) in glyphs.iter().enumerate() {
-            text.push_str(&glyph.substr);
-        }
-        text
-    }
-
-    fn delete_highlighted(&self, atom: &mut PropertyAtomicGuard) {
-        assert!(!self.selected.is_null(0).unwrap());
-        assert!(!self.selected.is_null(1).unwrap());
-
-        let start = self.selected.get_u32(0).unwrap() as usize;
-        let end = self.selected.get_u32(1).unwrap() as usize;
-
-        let sel_start = std::cmp::min(start, end);
-        let sel_end = std::cmp::max(start, end);
-
-        let mut glyphs = self.glyphs.lock().unwrap().clone();
-        glyphs.drain(sel_start..sel_end);
-
-        let text = Self::glyphs_to_string(&glyphs);
-        t!("delete_highlighted() text='{text}', cursor_pos={sel_start}");
-        self.text.set(atom, text);
-
-        self.selected.clone().set_null(atom, Role::Internal, 0).unwrap();
-        self.selected.clone().set_null(atom, Role::Internal, 1).unwrap();
-        self.cursor_pos.set(atom, sel_start as u32);
-    }
-
-    fn copy_highlighted(&self) -> Result<()> {
-        let start = self.selected.get_u32(0)? as usize;
-        let end = self.selected.get_u32(1)? as usize;
-
-        let sel_start = std::cmp::min(start, end);
-        let sel_end = std::cmp::max(start, end);
-
-        let mut text = String::new();
-
-        let glyphs = self.glyphs.lock().unwrap().clone();
-        for (glyph_idx, glyph) in glyphs.iter().enumerate() {
-            if sel_start <= glyph_idx && glyph_idx < sel_end {
-                text.push_str(&glyph.substr);
-            }
-        }
-
-        info!(target: "ui::editbox", "Copied '{}'", text);
-        window::clipboard_set(&text);
-        Ok(())
-    }
-
-    async fn paste_text(&self, key: String, atom: &mut PropertyAtomicGuard) {
-        let mut text = String::new();
-
-        let cursor_pos = self.cursor_pos.get();
-
-        if cursor_pos == 0 {
-            text = key.clone();
-        }
-
-        let glyphs = self.glyphs.lock().unwrap().clone();
-        for (glyph_idx, glyph) in glyphs.iter().enumerate() {
-            text.push_str(&glyph.substr);
-            if cursor_pos == glyph_idx as u32 + 1 {
-                text.push_str(&key);
-            }
-        }
-
-        self.text.set(atom, text);
-        // Not always true lol
-        self.cursor_pos.set(atom, cursor_pos + 1);
-
-        self.apply_cursor_scrolling(atom);
-        self.redraw().await;
-    }
-
-    async fn handle_touch_start(&self, pos: Point) -> bool {
-        t!("handle_touch_start({pos:?})");
-        let mut touch_info = self.touch_info.lock().unwrap();
-
-        if self.try_handle_drag(&mut touch_info, pos) {
-            return true
-        }
-
-        let rect = self.rect.get();
-        if !rect.contains(pos) {}
-
-        touch_info.start(pos);
-        true
-    }
-    fn try_handle_drag(&self, touch_info: &mut TouchInfo, pos: Point) -> bool {
-        let selections = self.select.lock().unwrap().clone();
-
-        if self.is_phone_select.load(Ordering::Relaxed) && selections.len() == 1 {
-            let select = selections.first().unwrap();
-
-            let rendered = {
-                let editable = self.editable.lock().unwrap();
-                editable.render()
-            };
-
-            let font_size = self.font_size.get();
-            let window_scale = self.window_scale.get();
-            let baseline = self.baseline.get();
-
-            // Get left handle centerpoint
-            let x1 = rendered.pos_to_xw(select.start, font_size, window_scale, baseline).0;
-            // Get right handle centerpoint
-            let x2 = {
-                let (x, w) = rendered.pos_to_xw(select.end, font_size, window_scale, baseline);
-                x + w
-            };
-
-            // Are we within range of either one?
-            let select_descent = self.select_descent.get();
-            let scroll = self.scroll.get();
-            let y = baseline + select_descent + 25.;
-
-            let p1 = Point::new(x1 - scroll, y);
-            let p2 = Point::new(x2 - scroll, y);
-            t!("handle center points = ({p1:?}, {p2:?})");
-
-            const TOUCH_RADIUS_SQ: f32 = 10_000.;
-            // Make pos relative to the rect
-            let pos_rel = pos - self.rect.get().pos();
-
-            if p1.dist_sq(pos_rel) <= TOUCH_RADIUS_SQ {
-                d!("TouchStateAction::DragSelectHandle [side=-1]");
-                // Set touch_state status to enable begin dragging them
-                touch_info.state = TouchStateAction::DragSelectHandle { side: -1 };
-                return true
-            }
-            if p2.dist_sq(pos_rel) <= TOUCH_RADIUS_SQ {
-                d!("TouchStateAction::DragSelectHandle [side=1]");
-                // Set touch_state status to enable begin dragging them
-                touch_info.state = TouchStateAction::DragSelectHandle { side: 1 };
-                return true
-            }
-        }
-
-        false
-    }
-
-    async fn handle_touch_move(&self, pos: Point) -> bool {
-        t!("handle_touch_move({pos:?})");
-        let atom = &mut PropertyAtomicGuard::none();
-        let touch_state = {
-            let mut touch_info = self.touch_info.lock().unwrap();
-            touch_info.update(&pos);
-            touch_info.state.clone()
-        };
-        match &touch_state {
-            TouchStateAction::Inactive => return false,
-            TouchStateAction::StartSelect => {
-                let x = pos.x;
-                self.start_touch_select(x);
-                self.redraw().await;
-                d!("TouchStateAction::Select");
-                self.touch_info.lock().unwrap().state = TouchStateAction::Select;
-            }
-            TouchStateAction::DragSelectHandle { side } => {
-                {
-                    assert!(*side == -1 || *side == 1);
-                    assert!(self.is_phone_select.load(Ordering::Relaxed));
-                    let mut selections = self.select.lock().unwrap();
-                    assert_eq!(selections.len(), 1);
-                    let select = selections.first_mut().unwrap();
-
-                    let rendered = {
-                        let editable = self.editable.lock().unwrap();
-                        editable.render()
-                    };
-
-                    let font_size = self.font_size.get();
-                    let window_scale = self.window_scale.get();
-                    let baseline = self.baseline.get();
-
-                    let pos_x = pos.x + self.scroll.get();
-
-                    let mut pos = rendered.x_to_pos(pos_x, font_size, window_scale, baseline);
-                    if *side == -1 {
-                        let select_other_pos = &mut select.end;
-                        if pos >= *select_other_pos {
-                            pos = *select_other_pos - 1;
-                        }
-                        select.start = pos;
-                    } else {
-                        let select_other_pos = &mut select.start;
-                        if pos <= *select_other_pos {
-                            pos = *select_other_pos + 1;
-                        }
-                        select.end = pos;
-                    }
-                }
-                self.redraw().await;
-            }
-            TouchStateAction::ScrollHoriz { start_pos, scroll_start } => {
-                let x_dist = start_pos.x - pos.x;
-                let mut scroll = scroll_start + x_dist;
-                scroll = scroll.clamp(0., self.max_cursor_scroll());
-                self.scroll.set(atom, scroll);
-                self.redraw().await;
-            }
-            _ => {}
-        }
-        true
-    }
-    async fn handle_touch_end(&self, pos: Point) -> bool {
-        t!("handle_touch_end({pos:?})");
-        let state = self.touch_info.lock().unwrap().stop();
-        match state {
-            TouchStateAction::Inactive => return false,
-            TouchStateAction::Started { pos: _, instant: _ } => {
-                let rect = self.rect.get();
-                let font_size = self.font_size.get();
-                let window_scale = self.window_scale.get();
-                let baseline = self.baseline.get();
-
-                {
-                    let mut editable = self.editable.lock().unwrap();
-                    let rendered = editable.render();
-                    // Adjust with scroll here too
-                    let x = pos.x - rect.x;
-
-                    let cpos = rendered.x_to_pos(x, font_size, window_scale, baseline);
-                    let cidx = rendered.pos_to_idx(cpos);
-                    editable.set_cursor_idx(cidx);
-
-                    // begin selection
-                    let mut select = self.select.lock().unwrap();
-                    select.clear();
-                }
-
-                self.is_phone_select.store(false, Ordering::Relaxed);
-                // Reshow cursor (if hidden)
-                self.hide_cursor.store(false, Ordering::Relaxed);
-
-                self.redraw().await;
-            }
-            _ => {}
-        }
-        window::show_keyboard(true);
-        true
-    }
-
-    /// Whenever the cursor property is modified this MUST be called
-    /// to recalculate the scroll x property.
-    fn apply_cursor_scrolling(&self, atom: &mut PropertyAtomicGuard) {
-        let rect = self.rect.get();
-
-        let cursor_pos = self.cursor_pos.get() as usize;
-        let cursor_width = self.cursor_width.get();
-        let mut scroll = self.scroll.get();
-
-        let cursor_x = {
-            let font_size = self.font_size.get();
-            let window_scale = self.window_scale.get();
-            let baseline = self.baseline.get();
-            let glyphs = self.glyphs.lock().unwrap().clone();
-
-            let mut glyph_pos_iter =
-                GlyphPositionIter::new(font_size, window_scale, &glyphs, baseline);
-
-            if cursor_pos == 0 {
-                0.
-            } else if cursor_pos == glyphs.len() {
-                let glyph_pos = glyph_pos_iter.last().unwrap();
-
-                let rhs = glyph_pos.rhs() + eol_nudge(font_size, &glyphs);
-                rhs
-            } else {
-                assert!(cursor_pos < glyphs.len());
-                let glyph_pos = glyph_pos_iter.nth(cursor_pos).expect("glyph pos mismatch glyphs");
-                glyph_pos.x
-            }
-        };
-
-        // The LHS and RHS of the cursor box
-        let cursor_lhs = cursor_x - scroll;
-        let cursor_rhs = cursor_lhs + cursor_width;
-
-        // RHS is outside
-        if cursor_rhs > rect.w {
-            // We want a scroll so RHS = w
-            // cursor_x - scroll + CURSOR_WIDTH = rect.w
-            scroll = cursor_x + cursor_width - rect.w;
-        // LHS is negative
-        } else if cursor_lhs < 0. {
-            // We want scroll so LHS = 0
-            // cursor_x - scroll = 0
-            scroll = cursor_x;
-        }
-
-        self.scroll.set(atom, scroll);
-    }
-
-    fn max_cursor_scroll(&self) -> f32 {
-        let font_size = self.font_size.get();
-        let window_scale = self.window_scale.get();
-        let baseline = self.baseline.get();
-        let rhs = {
-            let mut editable = self.editable.lock().unwrap();
-            let rendered = editable.render();
-
-            if rendered.glyphs.is_empty() {
-                return 0.
-            }
-            let glyph_pos_iter =
-                GlyphPositionIter::new(font_size, window_scale, &rendered.glyphs, baseline);
-            let last_rect = glyph_pos_iter.last().unwrap();
-            let mut rhs = last_rect.x + last_rect.w;
-            rhs += eol_nudge(font_size, &rendered.glyphs);
-            rhs
-        };
-
-        let rect_w = self.rect.get().w;
-        if rhs <= rect_w {
-            return 0.
-        }
-        let max_scroll = rhs - rect_w;
-        max_scroll
-    }
-
-    fn clamp_scroll(&self, atom: &mut PropertyAtomicGuard) {
-        let mut scroll = self.scroll.get();
-        scroll = scroll.clamp(0., self.max_cursor_scroll());
-        self.scroll.set(atom, scroll);
-    }
-
-    fn pause_blinking(&self) {
-        self.blink_is_paused.store(true, Ordering::Relaxed);
-        self.cursor_is_visible.store(true, Ordering::Relaxed);
-    }
-
-    async fn redraw(&self) {
-        let timest = unixtime();
-        t!("redraw()");
-
-        let parent_rect = self.parent_rect.lock().unwrap().unwrap().clone();
-        //self.rect.eval(&parent_rect).expect("unable to eval rect");
-
-        let Some(draw_update) = self.make_draw_calls() else {
-            error!(target: "ui::editbox", "Text failed to draw");
-            return
-        };
-
-        //self.render_api.replace_draw_calls(timest, draw_update.draw_calls);
-    }
-
-    fn redraw_cursor(&self) {
-        let timest = unixtime();
-        let cursor_instrs = self.get_cursor_instrs();
-
-        let draw_calls = vec![(
-            self.cursor_dc_key,
-            DrawCall::new(cursor_instrs, vec![], self.z_index.get(), "editbox_curs_redr"),
-        )];
-
-        //self.render_api.replace_draw_calls(timest, draw_calls);
-    }
-
-    fn get_cursor_instrs(&self) -> Vec<DrawInstruction> {
-        if !self.is_focused.get() ||
-            !self.cursor_is_visible.load(Ordering::Relaxed) ||
-            self.hide_cursor.load(Ordering::Relaxed)
-        {
-            return vec![]
-        }
-
-        let mut cursor_instrs = vec![];
-
-        let rect_w = self.rect.get().w;
-
-        let mut cursor_pos = Point::zero();
-        cursor_pos.x += self.cursor_px_offset();
-        if cursor_pos.x > rect_w {
-            return vec![]
-        }
-        cursor_instrs.push(DrawInstruction::Move(cursor_pos));
-
-        let cursor_mesh = {
-            let mut cursor_mesh = self.cursor_mesh.lock().unwrap();
-            if cursor_mesh.is_none() {
-                *cursor_mesh = Some(self.regen_cursor_mesh());
-            }
-            cursor_mesh.clone().unwrap()
-        };
-
-        cursor_instrs.push(DrawInstruction::Draw(cursor_mesh));
-
-        cursor_instrs
-    }
-
-    fn make_draw_calls(&self) -> Option<DrawUpdate> {
-        let rect = self.rect.get();
-
-        let text_mesh = self.regen_text_mesh(rect.clone());
-        let cursor_instrs = self.get_cursor_instrs();
-
-        Some(DrawUpdate {
-            key: self.text_dc_key,
-            draw_calls: vec![
-                (
-                    self.text_dc_key,
-                    DrawCall::new(
-                        vec![DrawInstruction::Move(rect.pos()), DrawInstruction::Draw(text_mesh)],
-                        vec![self.cursor_dc_key],
-                        self.z_index.get(),
-                        "editbox_txt",
-                    ),
-                ),
-                (
-                    self.cursor_dc_key,
-                    DrawCall::new(cursor_instrs, vec![], self.z_index.get(), "editbox_curs"),
-                ),
-            ],
-        })
-    }
-
-    async fn insert(&self, txt: &str, atom: &mut PropertyAtomicGuard) {
-        //let mut editor = self.lock_editor().await;
-        //editor.insert(txt, atom).await;
-    }
-}
-
-impl Drop for EditBox {
-    fn drop(&mut self) {
-        //self.render_api
-        //    .replace_draw_calls(unixtime(), vec![(self.text_dc_key, Default::default())]);
-    }
-}
-
-#[async_trait]
-impl UIObject for EditBox {
-    fn priority(&self) -> u32 {
-        self.priority.get()
-    }
-
-    async fn start(self: Arc<Self>, ex: ExecutorPtr) {
-        let me = Arc::downgrade(&self);
-
-        let mut on_modify = OnModify::new(ex.clone(), self.node.clone(), me.clone());
-        //on_modify.when_change(self.is_focused.prop(), Self::change_focus);
-
-        // When text has been changed.
-        // Cursor and selection might be invalidated.
-        async fn reset(self_: Arc<EditBox>, batch: BatchGuardPtr) {
-            self_.editable.lock().unwrap().end_compose();
-            self_.editable.lock().unwrap().set_text(self_.text.get(), String::new());
-            let atom = &mut PropertyAtomicGuard::none();
-            self_.cursor_pos.set(atom, 0);
-            self_.selected.clone().set_null(atom, Role::Internal, 0).unwrap();
-            self_.selected.clone().set_null(atom, Role::Internal, 1).unwrap();
-            self_.scroll.set(atom, 0.);
-            self_.redraw();
-        }
-        async fn redraw(self_: Arc<EditBox>, batch: BatchGuardPtr) {
-            self_.redraw().await;
-        }
-        on_modify.when_change(self.rect.prop(), redraw);
-        on_modify.when_change(self.baseline.prop(), redraw);
-        // The commented properties are modified on input events
-        // So then redraw() will get repeatedly triggered when these properties
-        // are changed. We should find a solution. For now the hooks are disabled.
-        //on_modify.when_change(scroll.prop(), redraw);
-        //on_modify.when_change(cursor_pos.prop(), redraw);
-        on_modify.when_change(self.font_size.prop(), redraw);
-        // We must also reshape text
-        on_modify.when_change(self.text.prop(), reset);
-        on_modify.when_change(self.text_color.prop(), redraw);
-        on_modify.when_change(self.hi_bg_color.prop(), redraw);
-        //on_modify.when_change(selected.clone(), redraw);
-        on_modify.when_change(self.z_index.prop(), redraw);
-        on_modify.when_change(self.debug.prop(), redraw);
-
-        async fn regen_cursor(self_: Arc<EditBox>, batch: BatchGuardPtr) {
-            let mesh = std::mem::take(&mut *self_.cursor_mesh.lock().unwrap());
-        }
-        on_modify.when_change(self.cursor_color.prop(), regen_cursor);
-        on_modify.when_change(self.cursor_ascent.prop(), regen_cursor);
-        on_modify.when_change(self.cursor_descent.prop(), regen_cursor);
-        on_modify.when_change(self.cursor_width.prop(), regen_cursor);
-
-        let me2 = me.clone();
-        let cursor_blink_time = self.cursor_blink_time.clone();
-        let cursor_idle_time = self.cursor_idle_time.clone();
-        let blinking_cursor_task = ex.spawn(async move {
-            loop {
-                msleep(cursor_blink_time.get() as u64).await;
-
-                let self_ = me2.upgrade().unwrap();
-
-                if self_.blink_is_paused.swap(false, Ordering::Relaxed) {
-                    msleep(cursor_idle_time.get() as u64).await;
-                    continue
-                }
-
-                // Invert the bool
-                self_.cursor_is_visible.fetch_not(Ordering::Relaxed);
-                self_.redraw_cursor();
-            }
-        });
-
-        let mut tasks = on_modify.tasks;
-        tasks.push(blinking_cursor_task);
-        self.tasks.set(tasks);
-    }
-
-    async fn draw(
-        &self,
-        parent_rect: Rectangle,
-        trace_id: u32,
-        atom: &mut PropertyAtomicGuard,
-    ) -> Option<DrawUpdate> {
-        t!("EditBox::draw() [trace_id={trace_id}]");
-
-        *self.parent_rect.lock().unwrap() = Some(parent_rect);
-        //self.rect.eval(&parent_rect).ok()?;
-
-        self.clamp_scroll(atom);
-        self.make_draw_calls()
-    }
-
-    async fn handle_char(&self, key: char, mods: KeyMods, repeat: bool) -> bool {
-        // First filter for only single digit keys
-        if DISALLOWED_CHARS.contains(&key) {
-            return false
-        }
-
-        if !self.is_focused.get() {
-            return false
-        }
-
-        let actions = {
-            let mut repeater = self.key_repeat.lock().unwrap();
-            repeater.key_down(PressedKey::Char(key), repeat)
-        };
-
-        let atom = &mut self.render_api.make_guard(gfxtag!("EditBox::handle_char"));
-
-        if mods.ctrl || mods.alt {
-            if repeat {
-                return false
-            }
-            self.handle_shortcut(key, &mods, atom).await;
-            return true
-        }
-
-        // Do nothing
-        if actions == 0 {
-            return true
-        }
-
-        t!("Key {key:?} has {actions} actions");
-        let key_str = key.to_string().repeat(actions as usize);
-        self.insert(&key_str, atom).await;
-        //self.redraw(atom).await;
-        true
-    }
-
-    async fn handle_key_down(&self, key: KeyCode, mods: KeyMods, repeat: bool) -> bool {
-        // First filter for only single digit keys
-        // Avoid processing events handled by insert_char()
-        if !ALLOWED_KEYCODES.contains(&key) {
-            return false
-        }
-
-        if !self.is_focused.get() {
-            return false
-        }
-
-        let actions = {
-            let mut repeater = self.key_repeat.lock().unwrap();
-            repeater.key_down(PressedKey::Key(key), repeat)
-        };
-
-        let atom = &mut PropertyAtomicGuard::none();
-
-        // Suppress noisy message
-        if actions > 0 {
-            t!("Key {key:?} has {actions} actions");
-        }
-        for _ in 0..actions {
-            self.handle_key(&key, &mods, atom).await;
-        }
-        true
-    }
-
-    async fn handle_mouse_btn_down(&self, btn: MouseButton, mouse_pos: Point) -> bool {
-        if !self.is_active.get() {
-            return false
-        }
-
-        if btn != MouseButton::Left {
-            return false
-        }
-
-        let rect = self.rect.get();
-        let atom = &mut PropertyAtomicGuard::none();
-
-        // clicking inside box will:
-        // 1. make it active
-        // 2. begin selection
-        if !rect.contains(mouse_pos) {
-            if self.is_focused.get() {
-                d!("EditBox unfocused");
-                self.is_focused.set(atom, false);
-                self.select.lock().unwrap().clear();
-
-                self.redraw().await;
-            }
-            return false
-        }
-
-        if self.is_focused.get() {
-            d!("EditBox clicked");
-        } else {
-            d!("EditBox focused");
-            self.is_focused.set(atom, true);
-        }
-
-        let font_size = self.font_size.get();
-        let window_scale = self.window_scale.get();
-        let baseline = self.baseline.get();
-
-        {
-            let mut editable = self.editable.lock().unwrap();
-            let rendered = editable.render();
-            let x = mouse_pos.x - rect.x + self.scroll.get();
-
-            let cpos = rendered.x_to_pos(x, font_size, window_scale, baseline);
-            let cidx = rendered.pos_to_idx(cpos);
-            editable.set_cursor_idx(cidx);
-
-            // begin selection
-            let mut select = self.select.lock().unwrap();
-            select.clear();
-            select.push(Selection::new(cpos, cpos));
-
-            self.mouse_btn_held.store(true, Ordering::Relaxed);
-        }
-
-        self.pause_blinking();
-        self.redraw().await;
-        true
-    }
-
-    async fn handle_mouse_btn_up(&self, btn: MouseButton, mouse_pos: Point) -> bool {
-        if !self.is_active.get() {
-            return false
-        }
-
-        // releasing mouse button will end selection
-        self.mouse_btn_held.store(false, Ordering::Relaxed);
-        false
-    }
-
-    async fn handle_mouse_move(&self, mouse_pos: Point) -> bool {
-        if !self.is_active.get() {
-            return false
-        }
-
-        let rect = self.rect.get();
-        self.is_mouse_hover.store(rect.contains(mouse_pos), Ordering::Relaxed);
-
-        if !self.mouse_btn_held.load(Ordering::Relaxed) {
-            return false
-        }
-
-        // if active and selection_active, then use x to modify the selection.
-        // also implement scrolling when cursor is to the left or right
-        // just scroll to the end
-        // also set cursor_pos too
-
-        let font_size = self.font_size.get();
-        let window_scale = self.window_scale.get();
-        let baseline = self.baseline.get();
-
-        {
-            let mut editable = self.editable.lock().unwrap();
-            let rendered = editable.render();
-            let x = mouse_pos.x - rect.x + self.scroll.get();
-
-            let cpos = rendered.x_to_pos(x, font_size, window_scale, baseline);
-            let cidx = rendered.pos_to_idx(cpos);
-            editable.set_cursor_idx(cidx);
-
-            // begin selection
-            let mut select = self.select.lock().unwrap();
-            select.last_mut().unwrap().end = cpos;
-        }
-
-        self.pause_blinking();
-        //self.apply_cursor_scrolling();
-        self.redraw().await;
-        true
-    }
-
-    async fn handle_mouse_wheel(&self, wheel_pos: Point) -> bool {
-        if !self.is_mouse_hover.load(Ordering::Relaxed) {
-            return false
-        }
-
-        let atom = &mut PropertyAtomicGuard::none();
-
-        let mut scroll = self.scroll.get() + wheel_pos.y * self.scroll_speed.get();
-        scroll = scroll.clamp(0., self.max_cursor_scroll());
-        t!("handle_mouse_wheel({wheel_pos:?}) [scroll={scroll}]");
-        self.scroll.set(atom, scroll);
-        self.redraw().await;
-
-        true
-    }
-
-    async fn handle_touch(&self, phase: TouchPhase, id: u64, touch_pos: Point) -> bool {
-        if !self.is_active.get() {
-            return false
-        }
-
-        // Ignore multi-touch
-        if id != 0 {
-            return false
-        }
-
-        match phase {
-            TouchPhase::Started => self.handle_touch_start(touch_pos).await,
-            TouchPhase::Moved => self.handle_touch_move(touch_pos).await,
-            TouchPhase::Ended => self.handle_touch_end(touch_pos).await,
-            TouchPhase::Cancelled => false,
-        }
-    }
-
-    /*
-    async fn handle_compose_text(
-        &self,
-        suggest_text: &str,
-        cursor_pos: i32,
-        is_commit: bool,
-    ) -> bool {
-        t!("handle_compose_text({suggest_text}, {is_commit})");
-
-        if !self.is_active.get() {
-            return false
-        }
-
-        {
-            let mut editable = self.editable.lock().unwrap();
-            editable.compose(suggest_text, is_commit);
-        }
-
-        let atom = &mut PropertyAtomicGuard::none();
-        //self.apply_cursor_scrolling();
-        self.clamp_scroll(atom);
-        self.redraw().await;
-
-        true
-    }
-    async fn handle_set_compose_region(&self, start: usize, end: usize) -> bool {
-        t!("handle_set_compose_region({start}, {end})");
-
-        if !self.is_active.get() {
-            return false
-        }
-
-        {
-            let mut editable = self.editable.lock().unwrap();
-            editable.set_compose_region(start, end);
-        }
-
-        self.redraw().await;
-
-        true
-    }
-    */
-}
-
-/// Filter these char events from being handled since we handle them
-/// using the key_up/key_down events.
-/// Avoids duplicate processing of keyboard input events.
-pub static DISALLOWED_CHARS: &'static [char] = &['\r', '\u{8}', '\u{7f}', '\t', '\n'];
-
-/// These keycodes are handled via normal key_up/key_down events.
-/// Anything in this list must be disallowed char events.
-pub static ALLOWED_KEYCODES: &'static [KeyCode] = &[
-    KeyCode::Left,
-    KeyCode::Right,
-    KeyCode::Up,
-    KeyCode::Down,
-    KeyCode::Enter,
-    KeyCode::Kp0,
-    KeyCode::Kp1,
-    KeyCode::Kp2,
-    KeyCode::Kp3,
-    KeyCode::Kp4,
-    KeyCode::Kp5,
-    KeyCode::Kp6,
-    KeyCode::Kp7,
-    KeyCode::Kp8,
-    KeyCode::Kp9,
-    KeyCode::KpDecimal,
-    KeyCode::KpEnter,
-    KeyCode::Delete,
-    KeyCode::Backspace,
-    KeyCode::Home,
-    KeyCode::End,
-];

+ 0 - 105
bin/app/src/ui/editbox/repeat.rs

@@ -1,105 +0,0 @@
-/* This file is part of DarkFi (https://dark.fi)
- *
- * Copyright (C) 2020-2025 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 miniquad::KeyCode;
-use std::{collections::HashMap, time::Instant};
-
-#[derive(Debug, Clone, Eq, Hash, PartialEq)]
-pub enum PressedKey {
-    Char(char),
-    Key(KeyCode),
-}
-
-/// On key press (repeat=false), we immediately process the event.
-/// Then there's a delay (repeat=true) and then for every step time
-/// while key press events are being sent, we allow an event.
-/// This ensures smooth typing in the editbox.
-pub struct PressedKeysSmoothRepeat {
-    /// When holding keys, we track from start and last sent time.
-    /// This is useful for initial delay and smooth scrolling.
-    pressed_keys: HashMap<PressedKey, RepeatingKeyTimer>,
-    /// Initial delay before allowing keys
-    start_delay: u32,
-    /// Minimum time between repeated keys
-    step_time: u32,
-}
-
-impl PressedKeysSmoothRepeat {
-    pub fn new(start_delay: u32, step_time: u32) -> Self {
-        Self { pressed_keys: HashMap::new(), start_delay, step_time }
-    }
-
-    pub fn clear(&mut self) {
-        self.pressed_keys.clear()
-    }
-
-    pub fn key_down(&mut self, key: PressedKey, repeat: bool) -> u32 {
-        trace!(target: "PressedKeysSmoothRepeat", "key_down({:?}, {})", key, repeat);
-
-        let is_initial_keypress = !repeat;
-        if is_initial_keypress {
-            trace!(target: "PressedKeysSmoothRepeat", "remove key {:?}", key);
-            self.pressed_keys.remove(&key);
-            return 1
-        }
-
-        // Insert key if not exists
-        if !self.pressed_keys.contains_key(&key) {
-            trace!(target: "PressedKeysSmoothRepeat", "insert key {:?}", key);
-            self.pressed_keys.insert(key.clone(), RepeatingKeyTimer::new());
-        }
-
-        let repeater = self.pressed_keys.get_mut(&key).expect("repeat map");
-        let actions = repeater.update(self.start_delay, self.step_time);
-        // This is a temporary workaround due to a miniquad issue.
-        // See https://github.com/not-fl3/miniquad/issues/517
-        std::cmp::min(1, actions)
-    }
-
-    /*
-    fn key_up(&mut self, key: &PressedKey) {
-        //trace!(target: "PressedKeysSmoothRepeat", "key_up({:?})", key);
-        assert!(self.pressed_keys.contains_key(key));
-        self.pressed_keys.remove(key).expect("key was pressed");
-    }
-    */
-}
-
-struct RepeatingKeyTimer {
-    start: Instant,
-    actions: u32,
-}
-
-impl RepeatingKeyTimer {
-    fn new() -> Self {
-        Self { start: Instant::now(), actions: 0 }
-    }
-
-    fn update(&mut self, start_delay: u32, step_time: u32) -> u32 {
-        let elapsed = self.start.elapsed().as_millis();
-        trace!(target: "RepeatingKeyTimer", "update() elapsed={}, actions={}",
-               elapsed, self.actions);
-        if elapsed < start_delay as u128 {
-            return 0
-        }
-        let total_actions = ((elapsed - start_delay as u128) / step_time as u128) as u32;
-        let remaining_actions = total_actions - self.actions;
-        self.actions = total_actions;
-        remaining_actions
-    }
-}

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

@@ -33,13 +33,8 @@ mod button;
 pub use button::{Button, ButtonPtr};
 pub mod chatview;
 pub use chatview::{ChatView, ChatViewPtr};
-mod baseedit;
-mod chatedit;
-pub use chatedit::{ChatEdit, ChatEditPtr};
 mod edit;
 pub use edit::{BaseEdit, BaseEditPtr, BaseEditType};
-mod editbox;
-pub use editbox::{EditBox, EditBoxPtr};
 pub mod emoji_picker;
 pub use emoji_picker::{EmojiPicker, EmojiPickerPtr};
 mod gesture;
@@ -203,8 +198,6 @@ pub fn get_ui_object_ptr(node: &SceneNode3) -> Arc<dyn UIObject + Send> {
         Pimpl::Layer(obj) => obj.clone(),
         Pimpl::VectorArt(obj) => obj.clone(),
         Pimpl::Text(obj) => obj.clone(),
-        Pimpl::EditBox(obj) => obj.clone(),
-        Pimpl::ChatEdit(obj) => obj.clone(),
         Pimpl::Edit(obj) => obj.clone(),
         Pimpl::ChatView(obj) => obj.clone(),
         Pimpl::Image(obj) => obj.clone(),
@@ -221,8 +214,6 @@ pub fn get_ui_object3<'a>(node: &'a SceneNode3) -> &'a dyn UIObject {
         Pimpl::Layer(obj) => obj.as_ref(),
         Pimpl::VectorArt(obj) => obj.as_ref(),
         Pimpl::Text(obj) => obj.as_ref(),
-        Pimpl::EditBox(obj) => obj.as_ref(),
-        Pimpl::ChatEdit(obj) => obj.as_ref(),
         Pimpl::Edit(obj) => obj.as_ref(),
         Pimpl::ChatView(obj) => obj.as_ref(),
         Pimpl::Image(obj) => obj.as_ref(),