Browse Source

app/edit: polish android editor behaviour

darkfi 1 week ago
parent
commit
89ef165018

+ 14 - 11
bin/app/Makefile

@@ -13,8 +13,8 @@ ZKAS = ../../zkas
 PROOFS_SRC = $(shell find ../../src/event_graph -type f -name '*.zk')
 PROOFS_SRC = $(shell find ../../src/event_graph -type f -name '*.zk')
 PROOFS_BIN = $(PROOFS_SRC:=.bin)
 PROOFS_BIN = $(PROOFS_SRC:=.bin)
 
 
-#ADB_DEVICE = -s DEVICE_ID
-ADB_DEVICE =
+ADB_DEVICE_INST =
+ADB_DEVICE_LOG  =
 
 
 SRC = \
 SRC = \
 	build.rs \
 	build.rs \
@@ -29,7 +29,7 @@ DEBUG_FEATURES = --features=enable-filelog,enable-plugins
 
 
 #DEV_FEATURES = --features=enable-filelog,enable-netdebug,emulate-android
 #DEV_FEATURES = --features=enable-filelog,enable-netdebug,emulate-android
 #DEV_FEATURES = --features=enable-filelog,enable-netdebug,enable-plugins
 #DEV_FEATURES = --features=enable-filelog,enable-netdebug,enable-plugins
-DEV_FEATURES = --features=schema-test-edit,enable-netdebug
+DEV_FEATURES = --features=schema-app,enable-netdebug,enable-plugin-drk
 
 
 default: build-release
 default: build-release
 	./darkfi-app
 	./darkfi-app
@@ -104,21 +104,24 @@ dev: $(SRC) fonts assets/forest_1920x1080.ivf
 	./darkfi-app
 	./darkfi-app
 
 
 # Users should use the android-release and android-debug targets instead.
 # Users should use the android-release and android-debug targets instead.
-apk: $(SRC) fonts assets/forest_720x1280.mp4
-	cargo quad-apk build --no-default-features $(DEV_FEATURES)
+apk:
+	$(MAKE) compile-apk
 	$(MAKE) install-apk
 	$(MAKE) install-apk
+	$(MAKE) log-apk
+
+compile-apk: $(SRC) fonts assets/forest_720x1280.mp4
+	cargo quad-apk build --no-default-features $(DEV_FEATURES)
 
 
 install-apk:
 install-apk:
 	-mv $(DEBUG_APK) .
 	-mv $(DEBUG_APK) .
-	-adb $(ADB_DEVICE) uninstall darkfi.darkfi_app
-	adb $(ADB_DEVICE) install -r darkfi-app.apk
-	$(MAKE) log-apk
+	-adb $(ADB_DEVICE_INST) uninstall darkfi.darkfi_app
+	adb $(ADB_DEVICE_INST) install -r darkfi-app.apk
 
 
 log-apk:
 log-apk:
 	reset
 	reset
-	adb $(ADB_DEVICE) logcat -c
-	adb $(ADB_DEVICE) shell monkey -p darkfi.darkfi_app -c android.intent.category.LAUNCHER 1
-	adb $(ADB_DEVICE) logcat -v color -s darkfi -s SAPP -s libc -s DEBUG -s ActivityManager -s ActivityTaskManager -s WindowManager -s AndroidRuntime -s rkfi.darkfi_app | tee output.log
+	adb $(ADB_DEVICE_LOG) logcat -c
+	adb $(ADB_DEVICE_LOG) shell monkey -p darkfi.darkfi_app -c android.intent.category.LAUNCHER 1
+	adb $(ADB_DEVICE_LOG) logcat -v color -s darkfi -s SAPP -s libc -s DEBUG -s ActivityManager -s ActivityTaskManager -s WindowManager -s AndroidRuntime -s rkfi.darkfi_app | tee output.log
 
 
 # Useful for dev
 # Useful for dev
 podman-cli:
 podman-cli:

+ 9 - 5
bin/app/src/android/textinput/gametextinput.rs

@@ -124,13 +124,15 @@ impl GameTextInput {
                 set_input_type_sig.as_ptr() as _,
                 set_input_type_sig.as_ptr() as _,
             );
             );
 
 
-            /*let restart_input_sig = b"()V\0";
+            /*
+            let restart_input_sig = b"()V\0";
             let restart_input_method = get_method_id(
             let restart_input_method = get_method_id(
                 env,
                 env,
                 input_connection_class,
                 input_connection_class,
                 b"restartInput\0".as_ptr() as _,
                 b"restartInput\0".as_ptr() as _,
                 restart_input_sig.as_ptr() as _,
                 restart_input_sig.as_ptr() as _,
-            );*/
+            );
+            */
 
 
             let state_class = new_global_ref!(env, state_java_class) as ndk_sys::jclass;
             let state_class = new_global_ref!(env, state_java_class) as ndk_sys::jclass;
 
 
@@ -234,7 +236,7 @@ impl GameTextInput {
 
 
     pub fn set_select(&self, text: &str, start: usize, end: usize) -> Result<(), ()> {
     pub fn set_select(&self, text: &str, start: usize, end: usize) -> Result<(), ()> {
         let Some(input_connection) = *self.input_connection.read() else {
         let Some(input_connection) = *self.input_connection.read() else {
-            w!("push_update() - no input_connection set");
+            w!("set_select() - no input_connection set");
             return Err(())
             return Err(())
         };
         };
         let start = byte_to_utf16(text, start) as i32;
         let start = byte_to_utf16(text, start) as i32;
@@ -328,7 +330,8 @@ impl GameTextInput {
         }
         }
     }
     }
 
 
-    /*pub fn restart_input(&self) {
+    /*
+    pub fn restart_input(&self) {
         let Some(input_connection) = *self.input_connection.read() else {
         let Some(input_connection) = *self.input_connection.read() else {
             w!("restart_input() - no input_connection set");
             w!("restart_input() - no input_connection set");
             return
             return
@@ -338,7 +341,8 @@ impl GameTextInput {
             let call_void_method = (**env).CallVoidMethod.unwrap();
             let call_void_method = (**env).CallVoidMethod.unwrap();
             call_void_method(env, input_connection, self.restart_input_method);
             call_void_method(env, input_connection, self.restart_input_method);
         }
         }
-    }*/
+    }
+    */
 
 
     fn state_to_java(&self, state: &AndroidTextInputState) -> ndk_sys::jobject {
     fn state_to_java(&self, state: &AndroidTextInputState) -> ndk_sys::jobject {
         unsafe {
         unsafe {

+ 14 - 4
bin/app/src/android/textinput/mod.rs

@@ -81,13 +81,21 @@ impl AndroidTextInput {
     pub fn show(&self) {
     pub fn show(&self) {
         t!("show IME");
         t!("show IME");
         let gti = GAME_TEXT_INPUT.get().unwrap();
         let gti = GAME_TEXT_INPUT.get().unwrap();
-        gti.focus(self.state.clone());
+        // Only call focus() (which pushes state via setState + restartInput)
+        // when this input was not already active. Re-focusing an already
+        // active editor (e.g. cursor moved by touch) must not restart the IME.
+        let is_active = self.state.lock().is_active;
+        if !is_active {
+            gti.focus(self.state.clone());
+        }
         gti.show_ime(0);
         gti.show_ime(0);
     }
     }
 
 
     pub fn hide(&self) {
     pub fn hide(&self) {
         t!("hide IME");
         t!("hide IME");
         let gti = GAME_TEXT_INPUT.get().unwrap();
         let gti = GAME_TEXT_INPUT.get().unwrap();
+        // Mark as inactive so the next show() will re-push state.
+        self.state.lock().is_active = false;
         gti.hide_ime(0);
         gti.hide_ime(0);
     }
     }
 
 
@@ -112,9 +120,12 @@ impl AndroidTextInput {
         }
         }
     }
     }
 
 
+    /// Lightweight selection-only update. Uses Java setSelection() directly
+    /// instead of setState() to avoid the expensive clear/clearSpans/insert/
+    /// restartInput cycle that corrupts SpannableStringBuilder when called
+    /// from a non-main thread. Matches the pattern in the official Java
+    /// processKeyEvent handler which also calls setSelection() directly.
     pub fn set_select(&self, select_start: usize, select_end: usize) {
     pub fn set_select(&self, select_start: usize, select_end: usize) {
-        //t!("set_select({select_start}, {select_end})");
-        // Always update our own state.
         let mut ours = self.state.lock();
         let mut ours = self.state.lock();
         let is_active = ours.is_active;
         let is_active = ours.is_active;
         let text = ours.state.text.clone();
         let text = ours.state.text.clone();
@@ -124,7 +135,6 @@ impl AndroidTextInput {
         state.select = (select_start, select_end);
         state.select = (select_start, select_end);
         drop(ours);
         drop(ours);
 
 
-        // Only update java state when this input is active
         if is_active {
         if is_active {
             let gti = GAME_TEXT_INPUT.get().unwrap();
             let gti = GAME_TEXT_INPUT.get().unwrap();
             gti.set_select(&text, select_start, select_end).unwrap();
             gti.set_select(&text, select_start, select_end).unwrap();

+ 74 - 4
bin/app/src/app/schema/test_edit.rs

@@ -21,15 +21,15 @@
 use crate::{
 use crate::{
     app::{
     app::{
         node::{
         node::{
-            create_chatview, create_layer, create_multiline_edit, create_text,
-            create_vector_art, create_video,
+            create_chatview, create_layer, create_multiline_edit, create_text, create_vector_art,
+            create_video,
         },
         },
         App,
         App,
     },
     },
     expr::{self, Compiler},
     expr::{self, Compiler},
     mesh::COLOR_PURPLE,
     mesh::COLOR_PURPLE,
     prop::{PropertyAtomicGuard, PropertyFloat32, PropertyStr, Role},
     prop::{PropertyAtomicGuard, PropertyFloat32, PropertyStr, Role},
-    scene::SceneNodePtr,
+    scene::{SceneNodePtr, Slot},
     ui::{BaseEdit, BaseEditType, ChatView, Layer, Text, VectorArt, VectorShape, Video},
     ui::{BaseEdit, BaseEditType, ChatView, Layer, Text, VectorArt, VectorShape, Video},
     util::i18n::I18nBabelFish,
     util::i18n::I18nBabelFish,
 };
 };
@@ -105,7 +105,6 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
 
 
     node.set_property_f32(atom, Role::App, "baseline", 40.).unwrap();
     node.set_property_f32(atom, Role::App, "baseline", 40.).unwrap();
     node.set_property_f32(atom, Role::App, "font_size", 50.).unwrap();
     node.set_property_f32(atom, Role::App, "font_size", 50.).unwrap();
-    //node.set_property_str(atom, Role::App, "text", "hello king!😁🍆jelly 🍆1234").unwrap();
     let prop = node.get_property("text_color").unwrap();
     let prop = node.get_property("text_color").unwrap();
     prop.set_f32(atom, Role::App, 0, 1.).unwrap();
     prop.set_f32(atom, Role::App, 0, 1.).unwrap();
     prop.set_f32(atom, Role::App, 1, 1.).unwrap();
     prop.set_f32(atom, Role::App, 1, 1.).unwrap();
@@ -162,4 +161,75 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
         .await;
         .await;
     let chatedit_node = node.clone();
     let chatedit_node = node.clone();
     layer_node.link(node);
     layer_node.link(node);
+
+    let (slot, recvr) = Slot::new("focus_request");
+    chatedit_node.register("focus_request", slot).unwrap();
+    let focus_task = ex.spawn(async move {
+        while let Ok(_) = recvr.recv().await {
+            chatedit_node.call_method("focus", vec![]).await.unwrap();
+        }
+    });
+    app.tasks.lock().unwrap().push(focus_task);
+
+    /*
+    #[cfg(target_os = "android")]
+    {
+        use crate::android::textinput::{AndroidTextInput, AndroidTextInputState};
+        use darkfi::system::msleep;
+
+        let (sender, recvr) = async_channel::unbounded::<AndroidTextInputState>();
+        let input = AndroidTextInput::new(sender);
+
+        let event_task = ex.spawn(async move {
+            loop {
+                match recvr.recv().await {
+                    Ok(state) => info!(target: "test_edit", "IME event: {state:?}"),
+                    Err(_) => break,
+                }
+            }
+        });
+        app.tasks.lock().unwrap().push(event_task);
+
+        let test_task = ex.spawn(async move {
+            msleep(3000).await;
+
+            info!(target: "test_edit", "=== STEP 1: show() ===");
+            input.show();
+            msleep(5000).await;
+
+            info!(target: "test_edit", "=== STEP 2: set_state(hello world) ===");
+            input.set_state(AndroidTextInputState {
+                text: "hello world".to_string(),
+                select: (11, 11),
+                compose: None,
+            });
+            msleep(5000).await;
+
+            info!(target: "test_edit", "=== STEP 3: set_select(1, 1) — cursor to start ===");
+            input.set_select(1, 1);
+            msleep(5000).await;
+
+            /*
+            info!(target: "test_edit", "=== STEP 4: set_select(6, 6) — cursor into 'world' ===");
+            input.set_select(6, 6);
+            msleep(5000).await;
+
+            info!(target: "test_edit", "=== STEP 5: set_select(0, 5) — select 'hello' ===");
+            input.set_select(0, 5);
+            msleep(5000).await;
+
+            info!(target: "test_edit", "=== STEP 6: hide() ===");
+            input.hide();
+            msleep(3000).await;
+
+            info!(target: "test_edit", "=== STEP 7: show() again ===");
+            input.show();
+            msleep(5000).await;
+            */
+
+            info!(target: "test_edit", "=== DONE ===");
+        });
+        app.tasks.lock().unwrap().push(test_task);
+    }
+    */
 }
 }

+ 0 - 3
bin/app/src/text/editor/android.rs

@@ -133,7 +133,6 @@ impl Editor {
         assert!(cursor_idx <= self.state.text.len());
         assert!(cursor_idx <= self.state.text.len());
         assert_eq!(self.state.text, self.text.get());
         assert_eq!(self.state.text, self.text.get());
         self.state.select = (cursor_idx, cursor_idx);
         self.state.select = (cursor_idx, cursor_idx);
-        self.state.compose = None;
         self.input.set_select(cursor_idx, cursor_idx);
         self.input.set_select(cursor_idx, cursor_idx);
     }
     }
 
 
@@ -217,14 +216,12 @@ impl Editor {
         assert!(select_end <= self.state.text.len());
         assert!(select_end <= self.state.text.len());
         assert_eq!(self.state.text, self.text.get());
         assert_eq!(self.state.text, self.text.get());
         self.state.select = (select_start, select_end);
         self.state.select = (select_start, select_end);
-        self.state.compose = None;
         self.input.set_select(select_start, select_end);
         self.input.set_select(select_start, select_end);
     }
     }
 
 
     pub fn select_all(&mut self) {
     pub fn select_all(&mut self) {
         let text_len = self.state.text.len();
         let text_len = self.state.text.len();
         self.state.select = (0, text_len);
         self.state.select = (0, text_len);
-        self.state.compose = None;
         self.input.set_select(0, text_len);
         self.input.set_select(0, text_len);
     }
     }
 
 

+ 20 - 12
bin/app/src/ui/edit/mod.rs

@@ -35,7 +35,7 @@ use std::{
 use tracing::instrument;
 use tracing::instrument;
 
 
 #[cfg(target_os = "android")]
 #[cfg(target_os = "android")]
-use crate::android::textinput::AndroidTextInputState;
+use crate::android::{textinput::AndroidTextInputState, is_ime_visible};
 use crate::{
 use crate::{
     clipboard,
     clipboard,
     gfx::{
     gfx::{
@@ -732,23 +732,17 @@ impl BaseEdit {
     /// This will select the entire word rather than move the cursor to that location
     /// This will select the entire word rather than move the cursor to that location
     fn start_touch_select(&self, touch_pos: Point, atom: &mut PropertyAtomicGuard) {
     fn start_touch_select(&self, touch_pos: Point, atom: &mut PropertyAtomicGuard) {
         ed!("start_touch_select({touch_pos:?}) before=[{}]", self.dbg_state());
         ed!("start_touch_select({touch_pos:?}) before=[{}]", self.dbg_state());
-        t!("start_touch_select({touch_pos:?})");
 
 
         let mut editor = self.editor.lock();
         let mut editor = self.editor.lock();
         editor.select_word_at_point(touch_pos);
         editor.select_word_at_point(touch_pos);
         editor.refresh();
         editor.refresh();
 
 
         let seltext = editor.selected_text().unwrap();
         let seltext = editor.selected_text().unwrap();
-        ed!("start_touch_select: word selected text={seltext:?} after=[{}]", self.dbg_state());
-        self.select_text.clone().set_str(atom, Role::Internal, 0, seltext).unwrap();
-
         drop(editor);
         drop(editor);
+        self.select_text.clone().set_str(atom, Role::Internal, 0, seltext).unwrap();
 
 
-        // if start != end {
-        t!("is_phone_select = true");
         self.is_phone_select.store(true, Ordering::Relaxed);
         self.is_phone_select.store(true, Ordering::Relaxed);
         self.hide_cursor.store(true, Ordering::Relaxed);
         self.hide_cursor.store(true, Ordering::Relaxed);
-        // }
     }
     }
 
 
     fn handle_touch_start(&self, touch_pos: Point) -> bool {
     fn handle_touch_start(&self, touch_pos: Point) -> bool {
@@ -997,8 +991,16 @@ impl BaseEdit {
         let scroll_sender = self.sel_sender.lock().clone().unwrap();
         let scroll_sender = self.sel_sender.lock().clone().unwrap();
         scroll_sender.try_send(None).unwrap();
         scroll_sender.try_send(None).unwrap();
 
 
-        let node = self.node();
-        node.trigger("focus_request", vec![]).await.unwrap();
+        let mut need_focus = !self.is_focused.get();
+        #[cfg(target_os = "android")]
+        if !is_ime_visible() {
+            need_focus = true;
+        }
+
+        if need_focus {
+            let node = self.node();
+            node.trigger("focus_request", vec![]).await.unwrap();
+        }
 
 
         true
         true
     }
     }
@@ -1017,7 +1019,10 @@ impl BaseEdit {
     }
     }
 
 
     fn finish_select(&self, atom: &mut PropertyAtomicGuard) {
     fn finish_select(&self, atom: &mut PropertyAtomicGuard) {
-        ed!("finish_select: phone_select={} -> false", self.is_phone_select.load(Ordering::Relaxed));
+        ed!(
+            "finish_select: phone_select={} -> false",
+            self.is_phone_select.load(Ordering::Relaxed)
+        );
         self.is_phone_select.store(false, Ordering::Release);
         self.is_phone_select.store(false, Ordering::Release);
         self.hide_cursor.store(false, Ordering::Release);
         self.hide_cursor.store(false, Ordering::Release);
         self.select_text.clone().set_null(atom, Role::Internal, 0).unwrap();
         self.select_text.clone().set_null(atom, Role::Internal, 0).unwrap();
@@ -1473,6 +1478,7 @@ impl BaseEdit {
         }
         }
 
 
         self_.editor.lock().focus();
         self_.editor.lock().focus();
+
         let atom = &mut self_.renderer.make_guard(gfxtag!("BaseEdit::process_focus_method"));
         let atom = &mut self_.renderer.make_guard(gfxtag!("BaseEdit::process_focus_method"));
         self_.is_focused.set(atom, true);
         self_.is_focused.set(atom, true);
         self_.redraw(atom);
         self_.redraw(atom);
@@ -1585,7 +1591,9 @@ impl BaseEdit {
             self.redraw_select(&self.renderer, atom.batch_id);
             self.redraw_select(&self.renderer, atom.batch_id);
             ed!("handle_android_event: handled select change, after=[{}]", self.dbg_state());
             ed!("handle_android_event: handled select change, after=[{}]", self.dbg_state());
         } else if is_compose_changed {
         } else if is_compose_changed {
-            ed!("handle_android_event: compose-only change ignored, after=[{}]", self.dbg_state());
+            self.editor.lock().refresh();
+            self.redraw(atom);
+            ed!("handle_android_event: handled compose change, after=[{}]", self.dbg_state());
         }
         }
     }
     }
 }
 }