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

wallet/editbox: long hold finger select words in editbox

darkfi 1 год назад
Родитель
Сommit
378328ca70

+ 14 - 0
bin/darkwallet/src/app/node.rs

@@ -175,6 +175,11 @@ pub fn create_editbox(name: &str) -> SceneNode {
     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.);
@@ -194,6 +199,15 @@ pub fn create_editbox(name: &str) -> SceneNode {
     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.);

+ 17 - 4
bin/darkwallet/src/app/schema.rs

@@ -52,6 +52,8 @@ mod ui_consts {
     pub const EDITCHAT_HEIGHT: f32 = 163.;
     pub const EDITCHAT_CURSOR_ASCENT: f32 = 50.;
     pub const EDITCHAT_CURSOR_DESCENT: f32 = 20.;
+    pub const EDITCHAT_SELECT_ASCENT: f32 = 60.;
+    pub const EDITCHAT_SELECT_DESCENT: f32 = 30.;
     pub const TEXTBAR_BASELINE: f32 = 93.;
     pub const EDITCHAT_LHS_PAD: f32 = 30.;
     pub const SENDLABEL_WIDTH: f32 = 200.;
@@ -72,6 +74,8 @@ mod ui_consts {
     pub const EDITCHAT_HEIGHT: f32 = 50.;
     pub const EDITCHAT_CURSOR_ASCENT: f32 = 25.;
     pub const EDITCHAT_CURSOR_DESCENT: f32 = 8.;
+    pub const EDITCHAT_SELECT_ASCENT: f32 = 30.;
+    pub const EDITCHAT_SELECT_DESCENT: f32 = 10.;
     pub const TEXTBAR_BASELINE: f32 = 34.;
     pub const EDITCHAT_LHS_PAD: f32 = 20.;
     pub const SENDLABEL_WIDTH: f32 = 120.;
@@ -320,6 +324,8 @@ pub(super) async fn make_test(app: &App, window: SceneNodePtr) {
     prop.set_f32(Role::App, 3, 1.).unwrap();
     node.set_property_f32(Role::App, "cursor_ascent", 40.).unwrap();
     node.set_property_f32(Role::App, "cursor_descent", 40.).unwrap();
+    node.set_property_f32(Role::App, "select_ascent", 60.).unwrap();
+    node.set_property_f32(Role::App, "select_descent", 60.).unwrap();
     let prop = node.get_property("hi_bg_color").unwrap();
     if LIGHTMODE {
         prop.set_f32(Role::App, 0, 0.5).unwrap();
@@ -804,6 +810,11 @@ pub(super) async fn make(app: &App, window: SceneNodePtr) {
         prop.set_f32(Role::App, 2, 1.).unwrap();
         prop.set_f32(Role::App, 3, 1.).unwrap();
     }
+    let prop = node.get_property("text_hi_color").unwrap();
+    prop.set_f32(Role::App, 0, 0.44).unwrap();
+    prop.set_f32(Role::App, 1, 0.96).unwrap();
+    prop.set_f32(Role::App, 2, 1.).unwrap();
+    prop.set_f32(Role::App, 3, 1.).unwrap();
     let prop = node.get_property("cursor_color").unwrap();
     prop.set_f32(Role::App, 0, 0.816).unwrap();
     prop.set_f32(Role::App, 1, 0.627).unwrap();
@@ -812,16 +823,18 @@ pub(super) async fn make(app: &App, window: SceneNodePtr) {
     node.set_property_f32(Role::App, "cursor_ascent", EDITCHAT_CURSOR_ASCENT).unwrap();
     node.set_property_f32(Role::App, "cursor_descent", EDITCHAT_CURSOR_DESCENT).unwrap();
     let prop = node.get_property("hi_bg_color").unwrap();
+    node.set_property_f32(Role::App, "select_ascent", EDITCHAT_SELECT_ASCENT).unwrap();
+    node.set_property_f32(Role::App, "select_descent", EDITCHAT_SELECT_DESCENT).unwrap();
     if LIGHTMODE {
         prop.set_f32(Role::App, 0, 0.5).unwrap();
         prop.set_f32(Role::App, 1, 0.5).unwrap();
         prop.set_f32(Role::App, 2, 0.5).unwrap();
         prop.set_f32(Role::App, 3, 1.).unwrap();
     } else {
-        prop.set_f32(Role::App, 0, 1.).unwrap();
-        prop.set_f32(Role::App, 1, 1.).unwrap();
-        prop.set_f32(Role::App, 2, 1.).unwrap();
-        prop.set_f32(Role::App, 3, 0.5).unwrap();
+        prop.set_f32(Role::App, 0, 0.).unwrap();
+        prop.set_f32(Role::App, 1, 0.27).unwrap();
+        prop.set_f32(Role::App, 2, 0.22).unwrap();
+        prop.set_f32(Role::App, 3, 1.).unwrap();
     }
     let prop = node.get_property("selected").unwrap();
     prop.set_null(Role::App, 0).unwrap();

+ 4 - 0
bin/darkwallet/src/gfx/linalg.rs

@@ -84,6 +84,10 @@ impl Point {
     pub fn to_rect(&self, w: f32, h: f32) -> Rectangle {
         Rectangle { x: self.x, y: self.y, w, h }
     }
+
+    pub fn dist_sq(&self, other: &Point) -> f32 {
+        (self.x - other.x).powi(2) + (self.y - other.y).powi(2)
+    }
 }
 
 impl From<[f32; 2]> for Point {

+ 343 - 0
bin/darkwallet/src/ui/editbox/editable.rs

@@ -0,0 +1,343 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use crate::{
+    prop::{
+        PropertyBool, PropertyColor, PropertyFloat32, PropertyPtr, PropertyRect, PropertyStr,
+        PropertyUint32, Role,
+    },
+    text::{self, Glyph, GlyphPositionIter, TextShaperPtr},
+    util::{enumerate_ref, is_whitespace},
+};
+
+pub(super) type TextPos = usize;
+pub(super) 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.compose_text) + &self.commit_text;
+        self.commit_text.clear();
+        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;
+    }
+}
+
+pub(super) 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()
+    }
+}
+
+/// 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(super) 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
+    fn end_compose(&mut self) {
+        #[cfg(target_os = "android")]
+        crate::android::cancel_composition();
+
+        let final_text = self.composer.clear();
+        self.before_text += &final_text;
+    }
+
+    fn get_text_before(&self) -> String {
+        let text =
+            self.before_text.clone() + &self.composer.commit_text + &self.composer.compose_text;
+        text
+    }
+    fn get_text(&self) -> String {
+        let text = self.get_text_before() + &self.after_text;
+        text
+    }
+
+    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();
+        chars.advance_back_by(before);
+        self.before_text = chars.as_str().to_string();
+
+        let mut chars = self.after_text.chars();
+        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,
+        )
+    }
+}
+
+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(Debug, Clone)]
+pub(super) struct Selection {
+    pub start: TextPos,
+    pub end: TextPos,
+}
+
+impl Selection {
+    pub fn new(start: TextPos, end: TextPos) -> Self {
+        Self { start, end }
+    }
+}

+ 214 - 427
bin/darkwallet/src/ui/editbox.rs → bin/darkwallet/src/ui/editbox/mod.rs

@@ -45,12 +45,15 @@ use crate::{
     pubsub::Subscription,
     scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
     text::{self, Glyph, GlyphPositionIter, TextShaperPtr},
-    util::{enumerate_ref, is_whitespace},
+    util::{enumerate_ref, is_whitespace, zip3},
     ExecutorPtr,
 };
 
 use super::{DrawUpdate, OnModify, UIObject};
 
+mod editable;
+use editable::{Editable, Selection, TextIdx, TextPos};
+
 // 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
@@ -140,433 +143,68 @@ impl RepeatingKeyTimer {
     }
 }
 
-/// 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 {
-    is_active: bool,
-    /// Glyph insertion point in the main text.
-    pos: usize,
-    /// Text that has been committed
-    commit_text: String,
-    /// Text that is being composed
-    compose_text: String,
-
-    region_start: usize,
-    region_end: usize,
-
-    glyphs: Vec<Glyph>,
-    text_shaper: TextShaperPtr,
+struct TouchStartInfo {
+    pos: Point,
+    instant: std::time::Instant,
 }
 
-impl ComposingText {
-    fn new(text_shaper: TextShaperPtr) -> Self {
-        Self {
-            is_active: false,
-            pos: 0,
-            commit_text: String::new(),
-            compose_text: String::new(),
-            region_start: 0,
-            region_end: 0,
-            glyphs: Vec::new(),
-            text_shaper,
-        }
-    }
-
-    /// When clearing, the glyphs should be taken and added to the main glyphs vec in parent
-    fn reset(&mut self) -> (usize, Vec<Glyph>) {
-        self.is_active = false;
-        self.commit_text.clear();
-        self.compose_text.clear();
-        self.region_start = 0;
-        self.region_end = 0;
-        (self.pos, std::mem::take(&mut self.glyphs))
-    }
-
-    /// If deactivated, then active at the cursor_pos
-    fn activate_or_cont(&mut self, cursor_pos: usize) {
-        if !self.is_active {
-            self.is_active = true;
-            self.pos = cursor_pos;
-        }
-    }
-
-    /// Total text to display.
-    fn get_text(&self) -> String {
-        self.commit_text.clone() + &self.compose_text
-    }
-
-    /// Set composing text.
-    fn compose(&mut self, text: String, font_size: f32, window_scale: f32) {
-        assert!(self.is_active);
-        self.compose_text = text;
-
-        self.region_start = self.commit_text.len();
-        self.region_end = self.region_start + self.compose_text.len();
-
-        let glyphs = self.text_shaper.shape(self.get_text(), font_size, window_scale);
-        self.glyphs = glyphs;
-    }
-
-    /// Commit the composing text.
-    fn commit(&mut self) {
-        assert!(self.is_active);
-        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;
-    }
-
-    fn glyph_compose_start(&self) -> usize {
-        self.convert_str_idx(self.region_start)
-    }
-    fn glyph_compose_end(&self) -> usize {
-        self.convert_str_idx(self.region_end)
-    }
-
-    fn has_compose(&self) -> bool {
-        self.region_start != self.region_end
-    }
-
-    /// Convert index in str to index in glyphs vec
-    fn convert_str_idx(&self, idx: usize) -> usize {
-        let mut curr_idx = 0;
-        for (i, glyph) in self.glyphs.iter().enumerate() {
-            curr_idx += glyph.substr.len();
-            if curr_idx > idx {
-                return i;
-            }
-        }
-        // We reached the end
-        return self.glyphs.len();
+impl TouchStartInfo {
+    fn new(pos: Point) -> Self {
+        Self { pos, instant: std::time::Instant::now() }
     }
 }
 
-/// 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 ComposingText2 {
-    /// Text that is being composed
-    compose_text: String,
-    /// Text that has been committed
-    commit_text: String,
-
-    region_start: usize,
-    region_end: usize,
+enum TouchStateAction {
+    Inactive,
+    Started { pos: Point, instant: std::time::Instant },
+    StartSelect,
+    Select,
+    ScrollUp,
+    MoveCursor,
 }
 
-impl ComposingText2 {
-    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.compose_text) + &self.commit_text;
-        self.commit_text.clear();
-        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;
-    }
+struct TouchInfo {
+    state: TouchStateAction,
+    start: Option<TouchStartInfo>,
 }
 
-struct RenderedEditable {
-    glyphs: Vec<Glyph>,
-    under_start: usize,
-    under_end: usize,
-    //select_start: usize,
-    //select_end: usize,
-}
-
-impl RenderedEditable {
-    fn new(glyphs: Vec<Glyph>, under_start: usize, under_end: usize) -> 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: usize) -> usize {
-        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
-    fn pos_to_idx(&self, pos: usize) -> usize {
-        let mut idx = 0;
-        for (i, glyph) in enumerate_ref(&self.glyphs) {
-            if i == pos {
-                return idx
-            }
-            idx += glyph.substr.len();
-        }
-        return idx;
-    }
-
-    fn has_underline(&self) -> bool {
-        self.under_start != self.under_end
-    }
-
-    /// Converts x offset to glyph pos. Will round to the closest side.
-    fn x_to_pos(&self, x: f32, font_size: f32, window_scale: f32, baseline: f32) -> usize {
-        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()
-    }
-}
-
-/// 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.
-struct Editable {
-    text_shaper: TextShaperPtr,
-
-    composer: ComposingText2,
-
-    before_text: String,
-    after_text: String,
-
-    font_size: PropertyFloat32,
-    window_scale: PropertyFloat32,
-    baseline: PropertyFloat32,
-}
-
-impl Editable {
-    fn new(
-        text_shaper: TextShaperPtr,
-
-        font_size: PropertyFloat32,
-        window_scale: PropertyFloat32,
-        baseline: PropertyFloat32,
-    ) -> Self {
-        Self {
-            text_shaper,
-            composer: ComposingText2::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
-    fn end_compose(&mut self) {
-        #[cfg(target_os = "android")]
-        crate::android::cancel_composition();
-
-        let final_text = self.composer.clear();
-        self.before_text += &final_text;
-    }
-
-    fn get_text_before(&self) -> String {
-        let text =
-            self.before_text.clone() + &self.composer.commit_text + &self.composer.compose_text;
-        text
-    }
-    fn get_text(&self) -> String {
-        let text = self.get_text_before() + &self.after_text;
-        text
+impl TouchInfo {
+    fn new() -> Self {
+        Self { state: TouchStateAction::Inactive, start: None }
     }
 
-    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();
-        }
+    fn start(&mut self, pos: Point) {
+        self.state = TouchStateAction::Started { pos, instant: std::time::Instant::now() };
     }
-    /// Convenience function
-    fn set_compose_region(&mut self, start: usize, end: usize) {
-        self.composer.set_compose_region(start, end);
-    }
-
-    fn delete(&mut self, before: usize, after: usize) {
-        self.end_compose();
-
-        let mut chars = self.before_text.chars();
-        chars.advance_back_by(before);
-        self.before_text = chars.as_str().to_string();
 
-        let mut chars = self.after_text.chars();
-        chars.advance_by(after);
-        self.after_text = chars.as_str().to_string();
+    fn stop(&mut self) -> TouchStateAction {
+        std::mem::replace(&mut self.state, TouchStateAction::Inactive)
     }
 
-    /// Move the cursor. This offset should be computed from the glyphs.
-    fn move_cursor(&mut self, dir: isize) {
-        self.end_compose();
+    fn update(&mut self, pos: &Point) {
+        match &self.state {
+            TouchStateAction::Started { pos: start_pos, instant } => {
+                let travel_dist = pos.dist_sq(&start_pos);
+                let y_dist = pos.y - start_pos.y;
+                let elapsed = instant.elapsed().as_millis();
 
-        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;
+                if travel_dist < 5. {
+                    if elapsed > 1000 {
+                        self.state = TouchStateAction::StartSelect;
+                        debug!(target: "ui::editbox", "touch long-hold select activated");
+                    }
+                } else if y_dist.abs() > 5. {
+                    self.state = TouchStateAction::ScrollUp;
+                    debug!(target: "ui::editbox", "touch scroll-up activated");
+                } else {
+                    self.state = TouchStateAction::MoveCursor;
+                    debug!(target: "ui::editbox", "move-cursor activated");
+                }
             }
+            _ => {}
         }
-
-        // Convert cursor pos to string idx
-        let idx = rendered.pos_to_idx(cursor_pos);
-        self.set_cursor_idx(idx);
-    }
-
-    fn set_cursor_idx(&mut self, idx: usize) {
-        // 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;
-    }
-
-    fn move_start(&mut self) {
-        self.end_compose();
-        self.after_text = self.get_text();
-        self.before_text.clear();
-    }
-    fn move_end(&mut self) {
-        self.end_compose();
-        self.before_text = self.get_text();
-        self.after_text.clear();
-    }
-
-    fn get_cursor_pos(&self, rendered: &RenderedEditable) -> usize {
-        let cursor_off = self.get_text_before().len();
-        let cursor_pos = rendered.idx_to_pos(cursor_off);
-        cursor_pos
-    }
-
-    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,
-        )
-    }
-}
-
-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(Debug, Clone)]
-struct Selection {
-    start: usize,
-    end: usize,
-}
-
-impl Selection {
-    fn new(start: usize, end: usize) -> Self {
-        Self { start, end }
     }
 }
 
@@ -585,6 +223,7 @@ pub struct EditBox {
     cursor_mesh: SyncMutex<Option<GfxDrawMesh>>,
     /// DC key for the cursor. Allows updating cursor independently.
     cursor_dc_key: u64,
+    popup_dc_key: u64,
 
     is_active: PropertyBool,
     is_focused: PropertyBool,
@@ -595,6 +234,7 @@ pub struct EditBox {
     font_size: PropertyFloat32,
     text: PropertyStr,
     text_color: PropertyColor,
+    text_hi_color: PropertyColor,
     cursor_color: PropertyColor,
     cursor_width: PropertyFloat32,
     cursor_ascent: PropertyFloat32,
@@ -602,17 +242,23 @@ pub struct EditBox {
     cursor_blink_time: PropertyUint32,
     cursor_idle_time: PropertyUint32,
     hi_bg_color: PropertyColor,
+    select_ascent: PropertyFloat32,
+    select_descent: PropertyFloat32,
     selected: PropertyPtr,
     z_index: PropertyUint32,
     debug: PropertyBool,
 
-    composer: SyncMutex<ComposingText>,
     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>,
+    phone_select: AtomicBool,
 
     old_window_scale: AtomicF32,
     window_scale: PropertyFloat32,
@@ -639,6 +285,7 @@ impl EditBox {
         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();
@@ -647,6 +294,10 @@ impl EditBox {
         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();
@@ -672,6 +323,7 @@ impl EditBox {
             text_dc_key: OsRng.gen(),
             cursor_mesh: SyncMutex::new(None),
             cursor_dc_key: OsRng.gen(),
+            popup_dc_key: OsRng.gen(),
 
             is_active,
             is_focused,
@@ -682,6 +334,7 @@ impl EditBox {
             font_size: font_size.clone(),
             text,
             text_color,
+            text_hi_color,
             cursor_color,
             cursor_width,
             cursor_ascent,
@@ -689,11 +342,12 @@ impl EditBox {
             cursor_blink_time,
             cursor_idle_time,
             hi_bg_color,
+            select_ascent,
+            select_descent,
             selected,
             z_index,
             debug,
 
-            composer: SyncMutex::new(ComposingText::new(text_shaper.clone())),
             editable: SyncMutex::new(Editable::new(
                 text_shaper,
                 font_size,
@@ -705,6 +359,10 @@ impl EditBox {
             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()),
+            phone_select: AtomicBool::new(false),
 
             old_window_scale: AtomicF32::new(window_scale.get()),
             window_scale,
@@ -728,6 +386,7 @@ impl EditBox {
         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;
@@ -743,9 +402,10 @@ impl EditBox {
         let mut mesh = MeshBuilder::new();
 
         let selections = self.select.lock().unwrap().clone();
-        for select in selections {
-            self.draw_selected(&mut mesh, &select, &rendered.glyphs, clip.h);
+        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(
@@ -760,8 +420,8 @@ impl EditBox {
         let glyph_pos_iter =
             GlyphPositionIter::new(font_size, window_scale, &rendered.glyphs, baseline);
 
-        for (glyph_idx, (mut glyph_rect, glyph)) in
-            glyph_pos_iter.zip(rendered.glyphs.iter()).enumerate()
+        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");
 
@@ -769,6 +429,9 @@ impl EditBox {
 
             //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;
             }
@@ -841,6 +504,19 @@ impl EditBox {
         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,
@@ -888,8 +564,15 @@ impl EditBox {
             end_x = rhs;
         }
 
-        // We don't need to do manual clipping since MeshBuilder should do that
-        let select_rect = Rectangle { x: start_x, y: 0., w: end_x - start_x, h: clip_h };
+        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());
     }
 
@@ -1075,6 +758,55 @@ impl EditBox {
         }
     }
 
+    /// 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();
+            let rendered = editable.render();
+            // Adjust with scroll here too
+            let x = x - rect.x;
+
+            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;
+                if is_whitespace(glyph_str) {
+                    break
+                }
+                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;
+                if is_whitespace(glyph_str) {
+                    break
+                }
+            }
+
+            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));
+        }
+
+        // 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() {
@@ -1155,6 +887,65 @@ impl EditBox {
         self.redraw().await;
     }
 
+    async fn handle_touch_start(&self, pos: Point) -> bool {
+        debug!(target: "ui::editbox", "handle_touch_start({pos:?})");
+        self.touch_info.lock().unwrap().start(pos.clone());
+        false
+    }
+    async fn handle_touch_move(&self, pos: Point) -> bool {
+        debug!(target: "ui::editbox", "handle_touch_move({pos:?})");
+        let touch_state = {
+            let mut touch_info = self.touch_info.lock().unwrap();
+            touch_info.update(&pos);
+            touch_info.state.clone()
+        };
+        match &touch_state {
+            TouchStateAction::StartSelect => {
+                let x = pos.x;
+                self.start_touch_select(x);
+                self.redraw().await;
+                self.touch_info.lock().unwrap().state = TouchStateAction::Select;
+            }
+            _ => {}
+        }
+        false
+    }
+    async fn handle_touch_end(&self, pos: Point) -> bool {
+        debug!(target: "ui::editbox", "handle_touch_end({pos:?})");
+        let state = self.touch_info.lock().unwrap().stop();
+        match state {
+            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();
+                }
+
+                // Reshow cursor (if hidden)
+                self.hide_cursor.store(false, Ordering::Relaxed);
+
+                self.redraw().await;
+            }
+            _ => {}
+        }
+        window::show_keyboard(true);
+        false
+    }
+
     /// Whenever the cursor property is modified this MUST be called
     /// to recalculate the scroll x property.
     fn apply_cursor_scrolling(&self) {
@@ -1232,7 +1023,10 @@ impl EditBox {
     }
 
     fn get_cursor_instrs(&self) -> Vec<GfxDrawInstruction> {
-        if !self.is_focused.get() || !self.cursor_is_visible.load(Ordering::Relaxed) {
+        if !self.is_focused.get() ||
+            !self.cursor_is_visible.load(Ordering::Relaxed) ||
+            self.hide_cursor.load(Ordering::Relaxed)
+        {
             return vec![]
         }
 
@@ -1453,8 +1247,6 @@ impl UIObject for EditBox {
             return false
         }
 
-        window::show_keyboard(true);
-
         if self.is_focused.get() {
             debug!(target: "ui::editbox", "EditBox clicked");
         } else {
@@ -1550,17 +1342,12 @@ impl UIObject for EditBox {
             return false
         }
 
-        /*
-        // Simulate mouse events
         match phase {
-            TouchPhase::Started => self.handle_click_down(MouseButton::Left, touch_pos).await,
-            TouchPhase::Moved => self.handle_cursor_move(touch_pos).await,
-            TouchPhase::Ended => self.handle_click_up(MouseButton::Left, touch_pos),
+            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,
         }
-        */
-
-        false
     }
 
     async fn handle_compose_text(&self, suggest_text: &str, is_commit: bool) -> bool {