Procházet zdrojové kódy

wallet/chatedit: proper touch drag select working

darkfi před 1 rokem
rodič
revize
3559ea0624

+ 8 - 0
bin/darkwallet/java/MainActivity.java

@@ -1,6 +1,7 @@
 //% IMPORTS
 
 import android.view.inputmethod.InputMethodManager;
+import android.os.Environment;
 
 import autosuggest.CustomInputConnection;
 
@@ -14,6 +15,13 @@ public void cancelComposition() {
     imm.restartInput(view);
 }
 
+public String getAppDataPath() {
+    return getApplicationContext().getDataDir().getAbsolutePath();
+}
+public String getExternalStoragePath() {
+    return Environment.getExternalStorageDirectory().getAbsolutePath();
+}
+
 //% END
 
 //% QUAD_SURFACE_ON_CREATE_INPUT_CONNECTION

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

@@ -331,6 +331,10 @@ pub fn create_chatedit(name: &str) -> SceneNode {
     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.);

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

@@ -52,6 +52,7 @@ mod android_ui_consts {
     pub const EDITCHAT_CURSOR_DESCENT: f32 = 20.;
     pub const EDITCHAT_SELECT_ASCENT: f32 = 40.;
     pub const EDITCHAT_SELECT_DESCENT: f32 = 8.;
+    pub const EDITCHAT_HANDLE_DESCENT: f32 = 33.;
     pub const TEXTBAR_BASELINE: f32 = 93.;
     pub const TEXT_DESCENT: f32 = 20.;
     pub const EDITCHAT_LHS_PAD: f32 = 30.;
@@ -95,6 +96,7 @@ mod ui_consts {
     pub const EDITCHAT_CURSOR_DESCENT: f32 = 8.;
     pub const EDITCHAT_SELECT_ASCENT: f32 = 30.;
     pub const EDITCHAT_SELECT_DESCENT: f32 = 10.;
+    pub const EDITCHAT_HANDLE_DESCENT: f32 = 35.;
     pub const TEXTBAR_BASELINE: f32 = 34.;
     pub const TEXT_DESCENT: f32 = 10.;
     pub const EDITCHAT_LHS_PAD: f32 = 20.;
@@ -265,7 +267,7 @@ pub(super) async fn make_test(app: &App, window: SceneNodePtr) {
     prop.set_f32(Role::App, 0, 0.).unwrap();
     prop.set_f32(Role::App, 1, 300. - 5.).unwrap();
     prop.set_expr(Role::App, 2, expr::load_var("w")).unwrap();
-    prop.set_f32(Role::App, 3, 200. + 10.).unwrap();
+    prop.set_f32(Role::App, 3, 400. + 10.).unwrap();
     node.set_property_u32(Role::App, "z_index", 2).unwrap();
 
     let mut shape = VectorShape::new();
@@ -516,7 +518,7 @@ pub(super) async fn make_test(app: &App, window: SceneNodePtr) {
     node.set_property_bool(Role::App, "is_active", true).unwrap();
     node.set_property_bool(Role::App, "is_focused", true).unwrap();
 
-    node.set_property_f32(Role::App, "max_height", 200.).unwrap();
+    node.set_property_f32(Role::App, "max_height", 400.).unwrap();
 
     let prop = node.get_property("rect").unwrap();
     prop.set_f32(Role::App, 0, 0.).unwrap();
@@ -525,9 +527,9 @@ pub(super) async fn make_test(app: &App, window: SceneNodePtr) {
     prop.set_f32(Role::App, 3, 50.).unwrap();
 
     node.set_property_f32(Role::App, "baseline", 34.).unwrap();
-    node.set_property_f32(Role::App, "linespacing", 34.).unwrap();
+    node.set_property_f32(Role::App, "linespacing", 50.).unwrap();
     node.set_property_f32(Role::App, "descent", 10.).unwrap();
-    node.set_property_f32(Role::App, "font_size", 20.).unwrap();
+    node.set_property_f32(Role::App, "font_size", 40.).unwrap();
     //node.set_property_str(Role::App, "text", "hello king!😁🍆jelly 🍆1234").unwrap();
     let prop = node.get_property("text_color").unwrap();
     prop.set_f32(Role::App, 0, 1.).unwrap();
@@ -548,6 +550,7 @@ pub(super) async fn make_test(app: &App, window: SceneNodePtr) {
     node.set_property_f32(Role::App, "cursor_descent", EDITCHAT_CURSOR_DESCENT).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();
+    node.set_property_f32(Role::App, "handle_descent", 25.).unwrap();
     let prop = node.get_property("hi_bg_color").unwrap();
     prop.set_f32(Role::App, 0, 0.5).unwrap();
     prop.set_f32(Role::App, 1, 0.5).unwrap();
@@ -944,6 +947,7 @@ pub(super) async fn make(app: &App, window: SceneNodePtr) {
     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();
+    node.set_property_f32(Role::App, "handle_descent", EDITCHAT_HANDLE_DESCENT).unwrap();
     if LIGHTMODE {
         prop.set_f32(Role::App, 0, 0.5).unwrap();
         prop.set_f32(Role::App, 1, 0.5).unwrap();

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

@@ -218,6 +218,9 @@ impl Rectangle {
     pub fn center(&self) -> Point {
         Point { x: self.x + self.w / 2., y: self.y + self.h / 2. }
     }
+    pub fn top_right(&self) -> Point {
+        Point { x: self.rhs(), y: self.y }
+    }
 
     pub fn dim(&self) -> Dimension {
         Dimension { w: self.w, h: self.h }

+ 4 - 4
bin/darkwallet/src/prop/wrap.rs

@@ -285,12 +285,12 @@ impl PropertyRect {
 
     pub fn eval(&self, parent_rect: &Rectangle) -> Result<()> {
         self.eval_with(
-            0..4,
+            (0..4).collect(),
             vec![("w".to_string(), parent_rect.w), ("h".to_string(), parent_rect.h)],
         )
     }
 
-    pub fn eval_with(&self, range: Range<usize>, extras: Vec<(String, f32)>) -> Result<()> {
+    pub fn eval_with(&self, range: Vec<usize>, extras: Vec<(String, f32)>) -> Result<()> {
         let mut globals = vec![];
 
         for dep in self.prop.get_depends() {
@@ -305,10 +305,10 @@ impl PropertyRect {
             globals.push((name, SExprVal::Float32(val)));
         }
 
-        debug!(target: "prop::wrap", "PropertyRect::eval() [globals = {globals:?}]");
+        //debug!(target: "prop::wrap", "PropertyRect::eval() [globals = {globals:?}]");
 
         let mut changes = vec![];
-        for i in range.clone() {
+        for i in range {
             if !self.prop.is_expr(i)? {
                 continue
             }

+ 121 - 105
bin/darkwallet/src/ui/chatedit.rs

@@ -155,6 +155,33 @@ impl TextWrap {
 
         cursor_pos
     }
+
+    fn get_word_boundary(&mut self, pos: TextPos) -> (TextPos, TextPos) {
+        let rendered = self.get_render();
+
+        // Find word start
+        let mut pos_start = pos;
+        while pos_start > 0 {
+            // Is the glyph before this pos just whitespace?
+            let glyph_str = &rendered.glyphs[pos_start - 1].substr;
+            if is_whitespace(glyph_str) {
+                break
+            }
+            pos_start -= 1;
+        }
+
+        // Find word end
+        let mut pos_end = pos;
+        while pos_end < rendered.glyphs.len() {
+            pos_end += 1;
+            let glyph_str = &rendered.glyphs[pos_end].substr;
+            if is_whitespace(glyph_str) {
+                break
+            }
+        }
+
+        (pos_start, pos_end)
+    }
 }
 
 struct WrappedLine {
@@ -226,14 +253,19 @@ impl WrappedLines {
     /// Convert an (x, y) point to a glyph pos
     fn point_to_pos(&self, mut point: Point) -> TextPos {
         let mut pos = 0;
-        for wrap_line in &self.lines {
+        for (line_idx, wrap_line) in self.lines.iter().enumerate() {
+            // Is it within this line?
             if point.y < self.linespacing {
+                debug!(target: "ui::editbox::wrapped_lines", "point to pos found line: {line_idx}");
                 pos += wrap_line.find_closest(point.x);
                 return pos
             }
+
+            // Continue to the next line
             point.y -= self.linespacing;
             pos += wrap_line.len();
         }
+        debug!(target: "ui::editbox::wrapped_lines", "point to pos using last line");
         pos
     }
 
@@ -258,13 +290,9 @@ impl WrappedLines {
         rhs
     }
 
-    fn get_glyph_pos(&self, mut pos: TextPos) -> Point {
-        if pos == 0 {
-            return Point::zero();
-        }
-
+    fn get_glyph_info(&self, mut pos: TextPos) -> (Rectangle, usize) {
         let mut y = 0.;
-        for wrap_line in &self.lines {
+        for (line_idx, wrap_line) in self.lines.iter().enumerate() {
             assert!(!wrap_line.glyphs.is_empty());
 
             if pos < wrap_line.len() {
@@ -272,10 +300,8 @@ impl WrappedLines {
                 let mut pos_iter = wrap_line.pos_iter();
                 pos_iter.advance_by(pos).unwrap();
 
-                let glyph_rect = pos_iter.next().unwrap();
-
-                let x = glyph_rect.x;
-                return Point::new(x, y)
+                let mut glyph_rect = pos_iter.next().unwrap();
+                return (glyph_rect, line_idx)
             }
 
             pos -= wrap_line.len();
@@ -284,7 +310,9 @@ impl WrappedLines {
 
         let rhs = self.last_rhs();
         let last_y = self.last_y();
-        Point::new(rhs, last_y)
+        let final_rect = Rectangle::new(rhs, last_y, 0., self.linespacing);
+        let last_idx = if self.lines.is_empty() { 0 } else { self.lines.len() - 1 };
+        (final_rect, last_idx)
     }
 }
 
@@ -403,6 +431,7 @@ pub struct ChatEdit {
     hi_bg_color: PropertyColor,
     select_ascent: PropertyFloat32,
     select_descent: PropertyFloat32,
+    handle_descent: PropertyFloat32,
     selected: PropertyPtr,
     z_index: PropertyUint32,
     debug: PropertyBool,
@@ -464,6 +493,8 @@ impl ChatEdit {
             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 selected = node_ref.get_property("selected").unwrap();
         let cursor_blink_time =
             PropertyUint32::wrap(node_ref, Role::Internal, "cursor_blink_time", 0).unwrap();
@@ -514,6 +545,7 @@ impl ChatEdit {
             hi_bg_color,
             select_ascent,
             select_descent,
+            handle_descent,
             selected,
             z_index,
             debug,
@@ -577,9 +609,13 @@ impl ChatEdit {
         let cursor_pos = self.cursor_pos.get() as usize;
         let cursor_color = self.cursor_color.get();
         let debug = self.debug.get();
-        //debug!(target: "ui::chatedit", "Rendering text '{text}' clip={clip:?}");
-        //debug!(target: "ui::chatedit", "    cursor_pos={cursor_pos}, is_focused={is_focused}");
 
+        let parent_rect = self.parent_rect.lock().clone().unwrap();
+        self.rect.eval_with(
+            vec![2],
+            vec![("parent_w".to_string(), parent_rect.w), ("parent_h".to_string(), parent_rect.h)],
+        );
+        // Height is calculated from width based on wrapping
         let width = self.wrap_width();
 
         let (atlas, wrapped_lines, selections) = {
@@ -599,7 +635,7 @@ impl ChatEdit {
         // Eval the rect
         let parent_rect = self.parent_rect.lock().clone().unwrap();
         self.rect.eval_with(
-            0..3,
+            vec![0, 1, 3],
             vec![
                 ("parent_w".to_string(), parent_rect.w),
                 ("parent_h".to_string(), parent_rect.h),
@@ -608,13 +644,14 @@ impl ChatEdit {
         );
 
         let mut clip = self.rect.get();
+        debug!(target: "ui::chatedit", "Rendering text '{text}' rect={clip:?} width={width}");
         clip.x = 0.;
         clip.y = 0.;
 
         let mut mesh = MeshBuilder::with_clip(clip.clone());
         let mut curr_y = -scroll;
 
-        debug!(target: "ui::chatedit", "regen_text_mesh() selections={selections:?}");
+        //debug!(target: "ui::chatedit", "regen_text_mesh() selections={selections:?}");
 
         for wrap_line in wrapped_lines.lines {
             // Just an assert
@@ -696,9 +733,11 @@ impl ChatEdit {
             (text_wrap.cursor_pos(), text_wrap.wrap(width))
         };
 
-        let mut point = wrapped_lines.get_glyph_pos(cursor_pos);
+        let glyph_info = wrapped_lines.get_glyph_info(cursor_pos);
+        let lineidx = glyph_info.1;
+        let mut point = glyph_info.0.pos();
         point.x = point.x.clamp(0., width);
-        point.y -= scroll;
+        point.y = lineidx as f32 * linespacing - scroll;
         point
     }
 
@@ -790,11 +829,11 @@ impl ChatEdit {
     }
 
     fn draw_phone_select_handle(&self, mesh: &mut MeshBuilder, x: f32, y_off: f32, side: f32) {
-        debug!(target: "ui::chatedit", "draw_phone_select_handle(..., {x}, {side})");
+        //debug!(target: "ui::chatedit", "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 handle_descent = self.handle_descent.get();
         let color = self.text_hi_color.get();
         // Transparent for fade
         let mut color_trans = color.clone();
@@ -813,12 +852,12 @@ impl ChatEdit {
                 uv: [0., 0.],
             },
             Vertex {
-                pos: [x - side * 1., y_off + baseline + select_descent + 5.],
+                pos: [x - side * 1., y_off + baseline + handle_descent + 5.],
                 color,
                 uv: [0., 0.],
             },
             Vertex {
-                pos: [x + side * 4., y_off + baseline + select_descent + 5.],
+                pos: [x + side * 4., y_off + baseline + handle_descent + 5.],
                 color,
                 uv: [0., 0.],
             },
@@ -826,7 +865,7 @@ impl ChatEdit {
         let indices = vec![0, 2, 1, 1, 2, 3];
         mesh.append(verts, indices);
 
-        let y = y_off + baseline + select_descent;
+        let y = y_off + baseline + handle_descent;
 
         // The arrow itself.
         // Go anti-clockwise
@@ -1035,6 +1074,7 @@ impl ChatEdit {
         move_cursor(&mut text_wrap.editable);
         let cursor_pos = text_wrap.editable.get_cursor_pos(&rendered);
         drop(text_wrap);
+        debug!(target: "ui::editbox", "Adjust cursor pos to {cursor_pos}");
 
         let mut select = self.select.lock();
 
@@ -1053,51 +1093,22 @@ impl ChatEdit {
     }
 
     /// 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 text_wrap = &mut self.text_wrap.lock();
-            text_wrap.clear_cache();
-            text_wrap.editable.end_compose();
-
-            let rendered = text_wrap.get_render();
-            let x = x - rect.x + self.scroll.get();
-
-            let cpos = rendered.x_to_pos(x, font_size, window_scale, baseline);
+    fn start_touch_select(&self, touch_pos: Point) {
+        let mut text_wrap = &mut self.text_wrap.lock();
+        text_wrap.clear_cache();
+        text_wrap.editable.end_compose();
 
-            // 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 width = self.wrap_width();
+        let wrapped_lines = text_wrap.wrap(width);
+        let pos = wrapped_lines.point_to_pos(touch_pos);
 
-            let cidx_start = rendered.pos_to_idx(cpos_start);
-            let cidx_end = rendered.pos_to_idx(cpos_end);
+        let (word_start, word_end) = text_wrap.get_word_boundary(pos);
 
-            // begin selection
-            let select = &mut text_wrap.select;
-            select.clear();
-            select.push(Selection::new(cpos_start, cpos_end));
-        }
+        // begin selection
+        let select = &mut text_wrap.select;
+        select.clear();
+        select.push(Selection::new(word_start, word_end));
+        debug!(target: "ui::chatview", "Selected {select:?} from {touch_pos:?}");
 
         self.is_phone_select.store(true, Ordering::Relaxed);
         // redraw() will now hide the cursor
@@ -1185,7 +1196,7 @@ impl ChatEdit {
     }
 
     async fn handle_touch_start(&self, mut touch_pos: Point) -> bool {
-        debug!(target: "ui::chatedit", "handle_touch_start({touch_pos:?})");
+        //debug!(target: "ui::chatedit", "handle_touch_start({touch_pos:?})");
         let mut touch_info = self.touch_info.lock();
 
         if self.try_handle_drag(&mut touch_info, touch_pos) {
@@ -1202,47 +1213,48 @@ impl ChatEdit {
         touch_info.start(touch_pos);
         true
     }
-    fn try_handle_drag(&self, touch_info: &mut TouchInfo, pos: Point) -> bool {
-        let selections = self.select.lock().clone();
+    fn try_handle_drag(&self, touch_info: &mut TouchInfo, mut touch_pos: Point) -> bool {
+        self.abs_to_local(&mut touch_pos);
+
+        let linespacing = self.linespacing.get();
+        let baseline = self.baseline.get();
+        let select_descent = self.select_descent.get();
+        //let scroll = self.scroll.get();
+
+        let mut text_wrap = self.text_wrap.lock();
+        let width = self.wrap_width();
+        let wrapped_lines = text_wrap.wrap(width);
+        let selections = &text_wrap.select;
 
         if self.is_phone_select.load(Ordering::Relaxed) && selections.len() == 1 {
             let select = selections.first().unwrap();
 
-            let rendered = self.text_wrap.lock().get_render().clone();
-
-            let font_size = self.font_size.get();
-            let window_scale = self.window_scale.get();
-            let baseline = self.baseline.get();
+            let handle_off_y = baseline + self.handle_descent.get();
 
             // Get left handle centerpoint
-            let x1 = rendered.pos_to_xw(select.start, font_size, window_scale, baseline).0;
+            let (glyph_rect, line_idx) = wrapped_lines.get_glyph_info(select.start);
+            let mut p1 = glyph_rect.pos();
+            // We always want the handles to be aligned so ignore the glyph's y pos
+            p1.y = line_idx as f32 * linespacing + handle_off_y;
+
             // Get right handle centerpoint
-            let x2 = {
-                let (x, w) = rendered.pos_to_xw(select.end, font_size, window_scale, baseline);
-                x + w
-            };
+            let (glyph_rect, line_idx) = wrapped_lines.get_glyph_info(select.end);
+            let mut p2 = glyph_rect.top_right();
+            p2.y = line_idx as f32 * linespacing + handle_off_y;
 
             // 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);
-            debug!(target: "ui::chatedit", "handle center points = ({p1:?}, {p2:?})");
+            //debug!(target: "ui::chatedit", "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 {
-                debug!(target: "ui::chatedit", "TouchStateAction::DragSelectHandle [side=-1]");
+            if p1.dist_sq(&touch_pos) <= TOUCH_RADIUS_SQ {
+                debug!(target: "ui::chatedit::touch", "start touch: DragSelectHandle state [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 {
-                debug!(target: "ui::chatedit", "TouchStateAction::DragSelectHandle [side=1]");
+            if p2.dist_sq(&touch_pos) <= TOUCH_RADIUS_SQ {
+                debug!(target: "ui::chatedit::touch", "start touch: DragSelectHandle state [side=1]");
                 // Set touch_state status to enable begin dragging them
                 touch_info.state = TouchStateAction::DragSelectHandle { side: 1 };
                 return true;
@@ -1263,29 +1275,32 @@ impl ChatEdit {
         match &touch_state {
             TouchStateAction::Inactive => return false,
             TouchStateAction::StartSelect => {
-                let x = touch_pos.x;
-                self.start_touch_select(x);
+                self.start_touch_select(touch_pos);
                 self.redraw().await;
-                debug!(target: "ui::chatedit", "TouchStateAction::Select");
+                debug!(target: "ui::chatedit::touch", "touch state: StartSelect -> Select");
                 self.touch_info.lock().state = TouchStateAction::Select;
             }
             TouchStateAction::DragSelectHandle { side } => {
                 {
+                    let linespacing = self.linespacing.get();
+                    let handle_descent = self.handle_descent.get();
+
+                    let mut text_wrap = self.text_wrap.lock();
+                    let width = self.wrap_width();
+                    let wrapped_lines = text_wrap.wrap(width);
+                    let rendered = text_wrap.get_render().clone();
+                    let selections = &mut text_wrap.select;
+
                     assert!(*side == -1 || *side == 1);
                     assert!(self.is_phone_select.load(Ordering::Relaxed));
-                    let mut selections = self.select.lock();
                     assert_eq!(selections.len(), 1);
                     let select = selections.first_mut().unwrap();
 
-                    let rendered = self.text_wrap.lock().get_render().clone();
-
-                    let font_size = self.font_size.get();
-                    let window_scale = self.window_scale.get();
-                    let baseline = self.baseline.get();
-
-                    let pos_x = touch_pos.x + self.scroll.get();
+                    let mut point = touch_pos;
+                    point.y -= linespacing + handle_descent;
+                    let mut pos = wrapped_lines.point_to_pos(point);
+                    debug!(target: "ui::chatedit", "desired pos = {point:?} [touch_pos={touch_pos:?}");
 
-                    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 {
@@ -1319,7 +1334,7 @@ impl ChatEdit {
         true
     }
     async fn handle_touch_end(&self, mut touch_pos: Point) -> bool {
-        debug!(target: "ui::chatedit", "handle_touch_end({touch_pos:?})");
+        //debug!(target: "ui::chatedit", "handle_touch_end({touch_pos:?})");
         self.abs_to_local(&mut touch_pos);
 
         let state = self.touch_info.lock().stop();
@@ -1425,7 +1440,7 @@ impl ChatEdit {
     }
 
     async fn redraw(&self) {
-        debug!(target: "ui::chatedit", "redraw()");
+        //debug!(target: "ui::chatedit", "redraw()");
 
         let Some(draw_update) = self.make_draw_calls() else {
             error!(target: "ui::chatedit", "Text failed to draw");
@@ -1701,6 +1716,7 @@ impl UIObject for ChatEdit {
         {
             let mut text_wrap = self.text_wrap.lock();
             let cursor_pos = text_wrap.set_cursor_with_point(mouse_pos, width);
+            debug!(target: "ui::editbox", "Mouse move cursor pos to {cursor_pos}");
 
             // begin selection
             let select = &mut text_wrap.select;

+ 7 - 1
bin/darkwallet/src/ui/editbox/editable.rs

@@ -370,7 +370,7 @@ fn glyphs_to_string(glyphs: &Vec<Glyph>) -> String {
     text
 }
 
-#[derive(Debug, Clone)]
+#[derive(Clone)]
 pub struct Selection {
     pub start: TextPos,
     pub end: TextPos,
@@ -381,3 +381,9 @@ impl Selection {
         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)
+    }
+}

+ 14 - 7
bin/darkwallet/src/ui/mod.rs

@@ -25,7 +25,7 @@ use crate::{
     error::{Error, Result},
     expr::{SExprMachine, SExprVal},
     gfx::{GfxBufferId, GfxDrawCall, GfxDrawMesh, GfxTextureId, Point, Rectangle},
-    prop::{PropertyPtr, Role},
+    prop::{ModifyAction, PropertyPtr, Role},
     scene::{Pimpl, SceneNode as SceneNode3, SceneNodeId, SceneNodePtr},
     ExecutorPtr,
 };
@@ -121,10 +121,10 @@ impl<T: Send + Sync + 'static> OnModify<T> {
         let node_name = self.node_name.clone();
         let node_id = self.node_id;
 
-        let mut on_modify_subs = vec![prop.subscribe_modify()];
+        let mut on_modify_subs = vec![(None, prop.subscribe_modify())];
         for dep in prop.get_depends() {
             let Some(dep_prop) = dep.prop.upgrade() else { continue };
-            on_modify_subs.push(dep_prop.subscribe_modify());
+            on_modify_subs.push((Some(dep.i), dep_prop.subscribe_modify()));
         }
 
         let prop_name = prop.name.clone();
@@ -132,15 +132,15 @@ impl<T: Send + Sync + 'static> OnModify<T> {
         let task = self.ex.spawn(async move {
             loop {
                 let mut poll_queues = FuturesUnordered::new();
-                for (i, on_modify_sub) in on_modify_subs.iter().enumerate() {
+                for (i, (prop_i, on_modify_sub)) in on_modify_subs.iter().enumerate() {
                     let recv = on_modify_sub.receive();
                     poll_queues.push(async move {
-                        let (role, _action) = recv.await.ok()?;
-                        Some((i, role))
+                        let (role, action) = recv.await.ok()?;
+                        Some((i, prop_i, role, action))
                     });
                 }
 
-                let Some(Some((idx, role))) = poll_queues.next().await else {
+                let Some(Some((idx, prop_i, role, action))) = poll_queues.next().await else {
                     error!(target: "app", "Property '{}':{}/'{}' on_modify pipe is broken", node_name, node_id, prop_name);
                     return
                 };
@@ -149,6 +149,13 @@ impl<T: Send + Sync + 'static> OnModify<T> {
                 if idx == 0 && role == Role::Internal {
                     continue
                 }
+                if let Some(prop_i) = prop_i {
+                    match action {
+                        ModifyAction::Set(i) => if *prop_i != i { continue },
+                        ModifyAction::SetCache(idxs) => if !idxs.contains(prop_i) { continue },
+                        _ => continue
+                    }
+                }
 
                 debug!(target: "app", "Property '{}':{}/'{}' modified", node_name, node_id, prop_name);