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

app: completely rework text editing subsystem. initial draft version

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

+ 25 - 7
bin/app/Cargo.lock

@@ -1304,6 +1304,7 @@ dependencies = [
  "miniquad",
  "miniquad",
  "parking_lot 0.12.3",
  "parking_lot 0.12.3",
  "parley",
  "parley",
+ "peniko",
  "rand",
  "rand",
  "regex",
  "regex",
  "rusqlite",
  "rusqlite",
@@ -1311,11 +1312,11 @@ dependencies = [
  "simplelog",
  "simplelog",
  "sled-overlay",
  "sled-overlay",
  "smol",
  "smol",
- "swash",
+ "swash 0.2.1",
  "thiserror 2.0.12",
  "thiserror 2.0.12",
  "tor-dirmgr 0.26.0",
  "tor-dirmgr 0.26.0",
  "url",
  "url",
- "zeno",
+ "zeno 0.3.2 (git+https://github.com/valadaptive/zeno?branch=tight-bounds)",
  "zeromq",
  "zeromq",
 ]
 ]
 
 
@@ -2025,7 +2026,7 @@ dependencies = [
 [[package]]
 [[package]]
 name = "fontique"
 name = "fontique"
 version = "0.3.0"
 version = "0.3.0"
-source = "git+https://github.com/linebender/parley.git#4bb6859668815e717491ce55e6fd9390e70b0e9a"
+source = "git+https://github.com/linebender/parley.git#d53a05820799a88b23a9712d9d026378a2699403"
 dependencies = [
 dependencies = [
  "bytemuck",
  "bytemuck",
  "fontconfig-cache-parser",
  "fontconfig-cache-parser",
@@ -3210,7 +3211,7 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
 [[package]]
 [[package]]
 name = "miniquad"
 name = "miniquad"
 version = "0.4.8"
 version = "0.4.8"
-source = "git+https://github.com/not-fl3/miniquad#613e9959c9cc7fca72c6c96eaf74ecced99165e3"
+source = "git+https://github.com/narodnik/miniquad#009352f4348f2d9ea9752246d219438d7af8c75c"
 dependencies = [
 dependencies = [
  "libc",
  "libc",
  "ndk-sys",
  "ndk-sys",
@@ -3628,13 +3629,13 @@ dependencies = [
 [[package]]
 [[package]]
 name = "parley"
 name = "parley"
 version = "0.3.0"
 version = "0.3.0"
-source = "git+https://github.com/linebender/parley.git#4bb6859668815e717491ce55e6fd9390e70b0e9a"
+source = "git+https://github.com/linebender/parley.git#d53a05820799a88b23a9712d9d026378a2699403"
 dependencies = [
 dependencies = [
  "fontique",
  "fontique",
  "hashbrown 0.15.2",
  "hashbrown 0.15.2",
  "peniko",
  "peniko",
  "skrifa",
  "skrifa",
- "swash",
+ "swash 0.2.2",
 ]
 ]
 
 
 [[package]]
 [[package]]
@@ -5007,7 +5008,18 @@ source = "git+https://github.com/valadaptive/swash?branch=tight-bounds#2a6429fb8
 dependencies = [
 dependencies = [
  "skrifa",
  "skrifa",
  "yazi",
  "yazi",
- "zeno",
+ "zeno 0.3.2 (git+https://github.com/valadaptive/zeno?branch=tight-bounds)",
+]
+
+[[package]]
+name = "swash"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fae9a562c7b46107d9c78cd78b75bbe1e991c16734c0aee8ff0ee711fb8b620a"
+dependencies = [
+ "skrifa",
+ "yazi",
+ "zeno 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 ]
 
 
 [[package]]
 [[package]]
@@ -7755,6 +7767,12 @@ dependencies = [
  "synstructure",
  "synstructure",
 ]
 ]
 
 
+[[package]]
+name = "zeno"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc0de2315dc13d00e5df3cd6b8d2124a6eaec6a2d4b6a1c5f37b7efad17fcc17"
+
 [[package]]
 [[package]]
 name = "zeno"
 name = "zeno"
 version = "0.3.2"
 version = "0.3.2"

+ 2 - 1
bin/app/Cargo.toml

@@ -9,7 +9,7 @@ homepage = "https://dark.fi"
 repository = "https://codeberg.org/darkrenaissance/darkfi"
 repository = "https://codeberg.org/darkrenaissance/darkfi"
 
 
 [dependencies]
 [dependencies]
-miniquad = { git = "https://github.com/not-fl3/miniquad" }
+miniquad = { git = "https://github.com/narodnik/miniquad" }
 
 
 # Currently latest version links to freetype-sys 0.19 but we use 0.21
 # Currently latest version links to freetype-sys 0.19 but we use 0.21
 #harfbuzz-sys = "0.6.1"
 #harfbuzz-sys = "0.6.1"
@@ -53,6 +53,7 @@ parley = { git = "https://github.com/linebender/parley.git" }
 swash = { git = "https://github.com/valadaptive/swash", branch = "tight-bounds" }
 swash = { git = "https://github.com/valadaptive/swash", branch = "tight-bounds" }
 # Use same zeno specified by swash
 # Use same zeno specified by swash
 zeno = { git = "https://github.com/valadaptive/zeno", branch = "tight-bounds" }
 zeno = { git = "https://github.com/valadaptive/zeno", branch = "tight-bounds" }
+peniko = "*"
 
 
 [features]
 [features]
 emulate-android = []
 emulate-android = []

+ 11 - 10
bin/app/Dockerfile

@@ -45,20 +45,21 @@ ENV NDK_HOME /usr/local/android-ndk-r25
 
 
 # Copy contents to container. Should only use this on a clean directory
 # Copy contents to container. Should only use this on a clean directory
 WORKDIR /root/
 WORKDIR /root/
-RUN git clone https://github.com/not-fl3/cargo-quad-apk cargo-apk
+#RUN git clone https://github.com/not-fl3/cargo-quad-apk cargo-apk
+RUN git clone --branch quad_injs https://github.com/narodnik/cargo-quad-apk cargo-apk
 # For deterministic builds, we want a deterministic toolchain
 # For deterministic builds, we want a deterministic toolchain
-RUN cd /root/cargo-apk && git checkout 8962f6888e748e201f6ac2ced411669a3f939e07
-
-# ArmV7a
-#ENV CC ${NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi31-clang
-#ENV CXX ${NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi31-clang++
-#ENV AR ${NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar
-#ENV CXXSTDLIB c++
-# Arm64V8a
-# X86
+#RUN cd /root/cargo-apk && git checkout 8962f6888e748e201f6ac2ced411669a3f939e07
 
 
 # Install binary
 # Install binary
 RUN cargo install --path /root/cargo-apk
 RUN cargo install --path /root/cargo-apk
 
 
 # Add build-tools to PATH, for apksigner
 # Add build-tools to PATH, for apksigner
 ENV PATH="/opt/android-sdk-linux/build-tools/31.0.0/:${PATH}"
 ENV PATH="/opt/android-sdk-linux/build-tools/31.0.0/:${PATH}"
+
+# Lets cache packages for faster builds
+RUN git clone https://codeberg.org/darkrenaissance/darkfi
+COPY Cargo.lock darkfi/bin/app/
+WORKDIR /root/darkfi/bin/app/
+RUN cargo fetch
+RUN rm -fr /root/darkfi/
+

+ 5 - 5
bin/app/Makefile

@@ -74,17 +74,17 @@ dev: $(SRC) fonts
 	./darkfi-app
 	./darkfi-app
 
 
 apk: $(SRC) fonts
 apk: $(SRC) fonts
-	podman run -v $(shell pwd)/../../:/root/darkfi -w /root/darkfi/bin/app/ -t apk cargo quad-apk build $(DEV_FEATURES)
+	podman run -v /home/narodnik/src/stuff/miniquad:/root/mq -v $(shell pwd)/../../:/root/darkfi -w /root/darkfi/bin/app/ -t apk cargo quad-apk build $(DEV_FEATURES)
 	-mv $(DEBUG_APK) .
 	-mv $(DEBUG_APK) .
-	-adb uninstall darkfi.app
-	adb install darkfi-app.apk
+	-adb uninstall darkfi.darkfi_app
+	adb install -r darkfi-app.apk
 	reset
 	reset
 	adb logcat -c
 	adb logcat -c
-	adb logcat -v color -s darkfi -s SAPP -s libc -s DEBUG -s ActivityManager -s ActivityTaskManager -s WindowManager
+	adb logcat -v color -s darkfi -s SAPP -s libc -s DEBUG -s ActivityManager -s ActivityTaskManager -s WindowManager -s AndroidRuntime -s rkfi.darkfi_app
 
 
 # Useful for dev
 # Useful for dev
 cli:
 cli:
-	podman run -v $(shell pwd)/../../:/root/darkfi -w /root/darkfi/bin/app/ -it apk bash
+	podman run -v /home/narodnik/src/stuff/parley:/root/parley -v /home/narodnik/src/stuff/miniquad:/root/mq -v $(shell pwd)/../../:/root/darkfi -w /root/darkfi/bin/app/ -it apk bash
 
 
 clean:
 clean:
 	podman run -v $(shell pwd):/root/dw -w /root/dw -t apk rm -fr target/
 	podman run -v $(shell pwd):/root/dw -w /root/dw -t apk rm -fr target/

+ 90 - 16
bin/app/java/MainActivity.java

@@ -1,26 +1,102 @@
 //% IMPORTS
 //% IMPORTS
 
 
-import android.view.inputmethod.InputMethodManager;
-import android.os.Environment;
+import android.view.ViewGroup;
+import android.text.Editable;
+import android.text.Spannable;
+import android.text.SpanWatcher;
+import android.text.Spanned;
+import android.text.TextWatcher;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.view.inputmethod.BaseInputConnection;
+import java.util.HashMap;
 
 
+import autosuggest.InvisibleInputView;
 import autosuggest.CustomInputConnection;
 import autosuggest.CustomInputConnection;
 
 
 //% END
 //% END
 
 
 //% MAIN_ACTIVITY_BODY
 //% MAIN_ACTIVITY_BODY
 
 
-public void cancelComposition() {
-    InputMethodManager imm =
-        (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
-    imm.restartInput(view);
+private ViewGroup rootView;
+
+private HashMap<Integer, InvisibleInputView> editors;
+
+native static void onInitEdit(int id);
+
+public void createComposer(final int id) {
+    Log.d("darkfi", "createComposer() -> " + id);
+
+    final InvisibleInputView iv = new InvisibleInputView(this, id);
+    editors.put(id, iv);
+
+    runOnUiThread(new Runnable() {
+        @Override
+        public void run() {
+            rootView.addView(iv);
+            iv.clearFocus();
+            onInitEdit(id);
+        }
+    });
+}
+
+public boolean focus(final int id) {
+    final InvisibleInputView iv = editors.get(id);
+    if (iv == null) {
+        return false;
+    }
+
+    runOnUiThread(new Runnable() {
+        @Override
+        public void run() {
+            boolean isFocused = iv.requestFocus();
+            // Just Android things ;)
+            if (!isFocused) {
+                Log.w("darkfi", "error requesting focus for id=" + id + ": " + iv);
+            }
+
+            InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
+            imm.showSoftInput(iv, InputMethodManager.SHOW_IMPLICIT);
+        }
+    });
+
+    return true;
 }
 }
 
 
+public CustomInputConnection getInputConnect(int id) {
+    InvisibleInputView iv = editors.get(id);
+    if (iv == null) {
+        return null;
+    }
+    return iv.inputConnection;
+}
+
+public boolean setText(final int id, final String txt) {
+    final InvisibleInputView iv = editors.get(id);
+    if (iv == null || iv.inputConnection == null) {
+        return false;
+    }
+
+    // Maybe do this on the UI thread?
+    iv.inputConnection.setEditableText(txt, txt.length(), txt.length(), 0, 0);
+
+    return true;
+}
+
+/*
+// Editable string with the spans displayed inline
+public String getDebugEditableStr() {
+    String edit = view.inputConnection.debugEditableStr();
+    Log.d("darkfi", "getDebugEditableStr() -> " + edit);
+    return edit;
+}
+*/
+
 public String getAppDataPath() {
 public String getAppDataPath() {
     return getApplicationContext().getDataDir().getAbsolutePath();
     return getApplicationContext().getDataDir().getAbsolutePath();
 }
 }
 public String getExternalStoragePath() {
 public String getExternalStoragePath() {
     return getApplicationContext().getExternalFilesDir(null).getAbsolutePath();
     return getApplicationContext().getExternalFilesDir(null).getAbsolutePath();
-    //return Environment.getExternalStorageDirectory().getAbsolutePath();
 }
 }
 
 
 public int getKeyboardHeight() {
 public int getKeyboardHeight() {
@@ -35,16 +111,14 @@ public int getKeyboardHeight() {
 
 
 //% END
 //% END
 
 
-//% QUAD_SURFACE_ON_CREATE_INPUT_CONNECTION
+//% MAIN_ACTIVITY_ON_CREATE
 
 
-// Needed to fix error: unreachable statement in Java
-if (true) {
-    outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT
-        | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT;
-    outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN
-        | EditorInfo.IME_ACTION_NONE;
-    return new CustomInputConnection(this, outAttrs);
-}
+rootView = layout;
+editors = new HashMap<>();
+
+view.setFocusable(false);
+view.setFocusableInTouchMode(false);
+view.clearFocus();
 
 
 //% END
 //% END
 
 

+ 87 - 21
bin/app/java/autosuggest/CustomInputConnection.java

@@ -44,8 +44,8 @@ import android.view.inputmethod.InputMethodManager;
 // It then adapts android's IME to chrome's RenderWidgetHostView using the
 // It then adapts android's IME to chrome's RenderWidgetHostView using the
 // native ImeAdapterAndroid via the outer class ImeAdapter.
 // native ImeAdapterAndroid via the outer class ImeAdapter.
 public class CustomInputConnection extends BaseInputConnection {
 public class CustomInputConnection extends BaseInputConnection {
-    private static final boolean DEBUG = false;
-    private int ID = (int)(Math.random() * (1 << 30));
+    private static final boolean DEBUG = true;
+    private int id = -1;
 
 
     private View mInternalView;
     private View mInternalView;
     //private ImeAdapter mImeAdapter;
     //private ImeAdapter mImeAdapter;
@@ -54,12 +54,15 @@ public class CustomInputConnection extends BaseInputConnection {
     private int numBatchEdits;
     private int numBatchEdits;
     private boolean shouldUpdateImeSelection;
     private boolean shouldUpdateImeSelection;
 
 
-    native static void onCompose(String text, int newCursorPos, boolean isCommit);
-    native static void onSetComposeRegion(int start, int end);
+    native static void onCompose(int id, String text, int newCursorPos, boolean isCommit);
+    native static void onSetComposeRegion(int id, int start, int end);
+    native static void onFinishCompose(int id);
+    native static void onDeleteSurroundingText(int id, int left, int right);
 
 
     //private AdapterInputConnection(View view, ImeAdapter imeAdapter, EditorInfo outAttrs) {
     //private AdapterInputConnection(View view, ImeAdapter imeAdapter, EditorInfo outAttrs) {
-    public CustomInputConnection(View view, EditorInfo outAttrs) {
+    public CustomInputConnection(int id, View view, EditorInfo outAttrs) {
         super(view, true);
         super(view, true);
+        this.id = id;
         log("CustomInputConnection()");
         log("CustomInputConnection()");
         mInternalView = view;
         mInternalView = view;
         //mImeAdapter = imeAdapter;
         //mImeAdapter = imeAdapter;
@@ -117,7 +120,7 @@ public class CustomInputConnection extends BaseInputConnection {
 
 
     private void log(String fstr, Object... args) {
     private void log(String fstr, Object... args) {
         if (!DEBUG) return;
         if (!DEBUG) return;
-        String text = "(" + ID + "): " + String.format(fstr, args);
+        String text = "(" + id + "): " + String.format(fstr, args);
         Log.d("darkfi", text);
         Log.d("darkfi", text);
     }
     }
 
 
@@ -138,11 +141,12 @@ public class CustomInputConnection extends BaseInputConnection {
      */
      */
     public void setEditableText(String text, int selectionStart, int selectionEnd,
     public void setEditableText(String text, int selectionStart, int selectionEnd,
             int compositionStart, int compositionEnd) {
             int compositionStart, int compositionEnd) {
-        log("darkfi", "setEditableText(%s, %d, %d, %d, %d)",
-            text, selectionStart, selectionEnd,
+        log("setEditableText(%s, %d, %d, %d, %d)", text,
+            selectionStart, selectionEnd,
             compositionStart, compositionEnd);
             compositionStart, compositionEnd);
 
 
         if (mEditable == null) {
         if (mEditable == null) {
+            log("setEditableText creating new editable");
             mEditable = Editable.Factory.getInstance().newEditable("");
             mEditable = Editable.Factory.getInstance().newEditable("");
         }
         }
 
 
@@ -176,12 +180,14 @@ public class CustomInputConnection extends BaseInputConnection {
         }
         }
 
 
         if (!textUnchanged) {
         if (!textUnchanged) {
+            log("replace mEditable with: %s", text);
             mEditable.replace(0, mEditable.length(), text);
             mEditable.replace(0, mEditable.length(), text);
         }
         }
         Selection.setSelection(mEditable, selectionStart, selectionEnd);
         Selection.setSelection(mEditable, selectionStart, selectionEnd);
         super.setComposingRegion(compositionStart, compositionEnd);
         super.setComposingRegion(compositionStart, compositionEnd);
 
 
         if (textUnchanged || prevText.equals("")) {
         if (textUnchanged || prevText.equals("")) {
+            log("setEditableText updating selection");
             // updateSelection should be called when a manual selection change occurs.
             // updateSelection should be called when a manual selection change occurs.
             // Should not be called if text is being entered else issues can occur
             // Should not be called if text is being entered else issues can occur
             // e.g. backspace to undo autocorrection will not work with the default OSK.
             // e.g. backspace to undo autocorrection will not work with the default OSK.
@@ -193,6 +199,7 @@ public class CustomInputConnection extends BaseInputConnection {
     @Override
     @Override
     public Editable getEditable() {
     public Editable getEditable() {
         if (mEditable == null) {
         if (mEditable == null) {
+            log("getEditable() [create new]");
             mEditable = Editable.Factory.getInstance().newEditable("");
             mEditable = Editable.Factory.getInstance().newEditable("");
             Selection.setSelection(mEditable, 0);
             Selection.setSelection(mEditable, 0);
         }
         }
@@ -205,7 +212,7 @@ public class CustomInputConnection extends BaseInputConnection {
         log("setComposingText(%s, %d)", text, newCursorPosition);
         log("setComposingText(%s, %d)", text, newCursorPosition);
         super.setComposingText(text, newCursorPosition);
         super.setComposingText(text, newCursorPosition);
         shouldUpdateImeSelection = true;
         shouldUpdateImeSelection = true;
-        onCompose(text.toString(), newCursorPosition, false);
+        onCompose(id, text.toString(), newCursorPosition, false);
         return true;
         return true;
     }
     }
 
 
@@ -214,7 +221,7 @@ public class CustomInputConnection extends BaseInputConnection {
         log("commitText(%s, %d)", text, newCursorPosition);
         log("commitText(%s, %d)", text, newCursorPosition);
         super.commitText(text, newCursorPosition);
         super.commitText(text, newCursorPosition);
         shouldUpdateImeSelection = true;
         shouldUpdateImeSelection = true;
-        onCompose(text.toString(), newCursorPosition, text.length() > 0);
+        onCompose(id, text.toString(), newCursorPosition, text.length() > 0);
         return true;
         return true;
     }
     }
 
 
@@ -258,6 +265,27 @@ public class CustomInputConnection extends BaseInputConnection {
         return false;
         return false;
     }
     }
 
 
+    @Override
+    public CharSequence getTextAfterCursor(int length, int flags) {
+        log("getTextAfterCursor(%d, %d)", length, flags);
+        return super.getTextAfterCursor(length, flags);
+    }
+    @Override
+    public CharSequence getTextBeforeCursor(int length, int flags) {
+        log("getTextBeforeCursor(%d, %d)", length, flags);
+        return super.getTextBeforeCursor(length, flags);
+    }
+    @Override
+    public SurroundingText getSurroundingText(int beforeLength, int afterLength, int flags) {
+        log("getSurroundingText(%d, %d, %d)", beforeLength, afterLength, flags);
+        return super.getSurroundingText(beforeLength, afterLength, flags);
+    }
+    @Override
+    public CharSequence getSelectedText(int flags) {
+        log("getSelectedText(%d)", flags);
+        return super.getSelectedText(flags);
+    }
+
     @Override
     @Override
     public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
     public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
         log("getExtractedText(...)");
         log("getExtractedText(...)");
@@ -282,6 +310,7 @@ public class CustomInputConnection extends BaseInputConnection {
         }
         }
         shouldUpdateImeSelection = true;
         shouldUpdateImeSelection = true;
         //return mImeAdapter.deleteSurroundingText(leftLength, rightLength);
         //return mImeAdapter.deleteSurroundingText(leftLength, rightLength);
+        onDeleteSurroundingText(id, leftLength, rightLength);
         return true;
         return true;
     }
     }
 
 
@@ -330,7 +359,7 @@ public class CustomInputConnection extends BaseInputConnection {
             return true;
             return true;
         }
         }
         super.finishComposingText();
         super.finishComposingText();
-        onCompose("", 0, true);
+        onFinishCompose(id);
         return true;
         return true;
     }
     }
 
 
@@ -360,7 +389,7 @@ public class CustomInputConnection extends BaseInputConnection {
         int a = Math.min(start, end);
         int a = Math.min(start, end);
         int b = Math.max(start, end);
         int b = Math.max(start, end);
         super.setComposingRegion(a, b);
         super.setComposingRegion(a, b);
-        onSetComposeRegion(a, b);
+        onSetComposeRegion(id, a, b);
         return true;
         return true;
     }
     }
 
 
@@ -369,19 +398,28 @@ public class CustomInputConnection extends BaseInputConnection {
     }
     }
 
 
     private InputMethodManager getInputMethodManager() {
     private InputMethodManager getInputMethodManager() {
-        return (InputMethodManager) mInternalView.getContext()
-                .getSystemService(Context.INPUT_METHOD_SERVICE);
+        InputMethodManager imm = (InputMethodManager)mInternalView.getContext()
+            .getSystemService(Context.INPUT_METHOD_SERVICE);
+        if (imm == null) {
+            Log.e("darkfi", "[IC]: InputMethodManager is NULL!");
+        }
+        return imm;
     }
     }
 
 
     private void updateImeSelection() {
     private void updateImeSelection() {
         log("updateImeSelection()");
         log("updateImeSelection()");
-        if (mEditable != null) {
-            getInputMethodManager().updateSelection(mInternalView,
-                    Selection.getSelectionStart(mEditable),
-                    Selection.getSelectionEnd(mEditable),
-                    getComposingSpanStart(mEditable),
-                    getComposingSpanEnd(mEditable));
+        if (mEditable == null) {
+            return;
         }
         }
+
+        getInputMethodManager().updateSelection(
+            mInternalView,
+            Selection.getSelectionStart(mEditable),
+            Selection.getSelectionEnd(mEditable),
+            getComposingSpanStart(mEditable),
+            getComposingSpanEnd(mEditable)
+        );
+        log("updateImeSelection() DONE");
     }
     }
 
 
     @Override
     @Override
@@ -398,6 +436,7 @@ public class CustomInputConnection extends BaseInputConnection {
             updateImeSelection();
             updateImeSelection();
             shouldUpdateImeSelection = false;
             shouldUpdateImeSelection = false;
         }
         }
+        log("endBatchEdit DONE");
         return false;
         return false;
     }
     }
 
 
@@ -419,7 +458,15 @@ public class CustomInputConnection extends BaseInputConnection {
             }
             }
 
 
             // Append the character
             // Append the character
-            xmlBuilder.append(editable.charAt(i));
+            char c = editable.charAt(i);
+            xmlBuilder.append(c);
+
+            if (Character.isHighSurrogate(c)) {
+                if (i + 1 < editable.length() && Character.isLowSurrogate(editable.charAt(i + 1))) {
+                    i += 1;
+                    xmlBuilder.append(editable.charAt(i));
+                }
+            }
 
 
             // Find spans ending at this position
             // Find spans ending at this position
             for (Object span : spans) {
             for (Object span : spans) {
@@ -453,5 +500,24 @@ public class CustomInputConnection extends BaseInputConnection {
 
 
         return xmlBuilder.toString();
         return xmlBuilder.toString();
     }
     }
+
+    public String debugEditableStr() {
+        return editableToXml(mEditable);
+    }
+    public String rawText() {
+        return mEditable.toString();
+    }
+    public int getSelectionStart() {
+        return Selection.getSelectionStart(mEditable);
+    }
+    public int getSelectionEnd() {
+        return Selection.getSelectionEnd(mEditable);
+    }
+    public int getComposeStart() {
+        return getComposingSpanStart(mEditable);
+    }
+    public int getComposeEnd() {
+        return getComposingSpanEnd(mEditable);
+    }
 }
 }
 
 

+ 90 - 0
bin/app/java/autosuggest/InvisibleInputView.java

@@ -0,0 +1,90 @@
+/* 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/>.
+ */
+
+package autosuggest;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+import autosuggest.CustomInputConnection;
+
+public class InvisibleInputView extends View {
+    public CustomInputConnection inputConnection;
+    public int id = -1;
+
+    native static void onCreateInputConnect(int id);
+
+    public InvisibleInputView(Context ctx, int id) {
+        super(ctx);
+        setFocusable(true);
+        setFocusableInTouchMode(true);
+        //setVisibility(INVISIBLE);
+        setVisibility(VISIBLE);
+        //setAlpha(0f);
+        setLayoutParams(new ViewGroup.LayoutParams(400, 200));
+        this.id = id;
+    }
+
+    /*
+    @Override
+    protected void onDraw(Canvas canvas) {
+        Log.d("darkfi", "InvisibleInputView skipping onDraw()");
+    }
+    */
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        Log.d("darkfi", "InvisibleInputView " + id + " attached to window");
+    }
+    @Override
+    public boolean onCheckIsTextEditor() {
+        Log.d("darkfi", "onCheckIsTextEditor");
+        return true;
+    }
+
+    @Override
+    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+        Log.d("darkfi", "Create InputConnection for view=" + this.toString());
+        if (inputConnection != null) {
+            Log.d("darkfi", "  ->  return existing InputConnection");
+            return inputConnection;
+        }
+
+        outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT;
+        outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN
+            | EditorInfo.IME_ACTION_NONE;
+        inputConnection = new CustomInputConnection(id, this, outAttrs);
+        onCreateInputConnect(id);
+        return inputConnection;
+    }
+
+    @Override
+    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+        Log.d("darkfi", "onFocusChanged: " + gainFocus);
+    }
+}
+

+ 2 - 0
bin/app/quad.toml

@@ -1,5 +1,7 @@
 main_activity_inject = "java/MainActivity.java"
 main_activity_inject = "java/MainActivity.java"
 java_files = [
 java_files = [
     "java/autosuggest/CustomInputConnection.java",
     "java/autosuggest/CustomInputConnection.java",
+    "java/autosuggest/InvisibleInputView.java",
+    #"java/autosuggest/InvisibleInputManager.java",
 ]
 ]
 
 

+ 212 - 42
bin/app/src/android.rs

@@ -22,107 +22,277 @@ use std::{
     sync::{LazyLock, Mutex as SyncMutex},
     sync::{LazyLock, Mutex as SyncMutex},
 };
 };
 
 
+use crate::AndroidSuggestEvent;
+
+macro_rules! call_mainactivity_int_method {
+    ($method:expr, $sig:expr $(, $args:expr)*) => {{
+        unsafe {
+            let env = android::attach_jni_env();
+            ndk_utils::call_int_method!(env, android::ACTIVITY, $method, $sig $(, $args)*)
+        }
+    }};
+}
+macro_rules! call_mainactivity_str_method {
+    ($method:expr) => {{
+        unsafe {
+            let env = android::attach_jni_env();
+            let text = ndk_utils::call_object_method!(
+                env,
+                android::ACTIVITY,
+                $method,
+                "()Ljava/lang/String;"
+            );
+            ndk_utils::get_utf_str!(env, text)
+        }
+    }};
+}
+
 struct GlobalData {
 struct GlobalData {
     sender: Option<async_channel::Sender<AndroidSuggestEvent>>,
     sender: Option<async_channel::Sender<AndroidSuggestEvent>>,
+    next_id: usize,
 }
 }
 
 
 unsafe impl Send for GlobalData {}
 unsafe impl Send for GlobalData {}
 unsafe impl Sync for GlobalData {}
 unsafe impl Sync for GlobalData {}
 
 
 static GLOBALS: LazyLock<SyncMutex<GlobalData>> =
 static GLOBALS: LazyLock<SyncMutex<GlobalData>> =
-    LazyLock::new(|| SyncMutex::new(GlobalData { sender: None }));
+    LazyLock::new(|| SyncMutex::new(GlobalData { sender: None, next_id: 0 }));
+
+#[no_mangle]
+pub unsafe extern "C" fn Java_darkfi_darkfi_1app_MainActivity_onInitEdit(
+    env: *mut ndk_sys::JNIEnv,
+    _: ndk_sys::jobject,
+    id: ndk_sys::jint,
+) {
+    trace!(target: "android", "onInit() CALLED");
+    assert!(id >= 0);
+    let id = id as usize;
+    if let Some(sender) = &GLOBALS.lock().unwrap().sender {
+        trace!(target: "android", "onInit()");
+        let _ = sender.try_send(AndroidSuggestEvent::Init);
+    }
+}
 
 
-pub enum AndroidSuggestEvent {
-    Compose { text: String, cursor_pos: i32, is_commit: bool },
-    ComposeRegion { start: usize, end: usize },
+#[no_mangle]
+pub unsafe extern "C" fn Java_autosuggest_InvisibleInputView_onCreateInputConnect(
+    env: *mut ndk_sys::JNIEnv,
+    _: ndk_sys::jobject,
+    id: ndk_sys::jint,
+) {
+    assert!(id >= 0);
+    let id = id as usize;
+    if let Some(sender) = &GLOBALS.lock().unwrap().sender {
+        let _ = sender.try_send(AndroidSuggestEvent::CreateInputConnect);
+    }
 }
 }
 
 
 #[no_mangle]
 #[no_mangle]
 pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_onCompose(
 pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_onCompose(
     env: *mut ndk_sys::JNIEnv,
     env: *mut ndk_sys::JNIEnv,
     _: ndk_sys::jobject,
     _: ndk_sys::jobject,
+    id: ndk_sys::jint,
     text: ndk_sys::jobject,
     text: ndk_sys::jobject,
     cursor_pos: ndk_sys::jint,
     cursor_pos: ndk_sys::jint,
     is_commit: ndk_sys::jboolean,
     is_commit: ndk_sys::jboolean,
 ) {
 ) {
+    assert!(id >= 0);
+    let id = id as usize;
     let text = ndk_utils::get_utf_str!(env, text);
     let text = ndk_utils::get_utf_str!(env, text);
     if let Some(sender) = &GLOBALS.lock().unwrap().sender {
     if let Some(sender) = &GLOBALS.lock().unwrap().sender {
         let _ = sender.try_send(AndroidSuggestEvent::Compose {
         let _ = sender.try_send(AndroidSuggestEvent::Compose {
             text: text.to_string(),
             text: text.to_string(),
             cursor_pos,
             cursor_pos,
-            is_commit: is_commit != 0,
+            is_commit: is_commit == 1,
         });
         });
     }
     }
 }
 }
-
 #[no_mangle]
 #[no_mangle]
 pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_onSetComposeRegion(
 pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_onSetComposeRegion(
     env: *mut ndk_sys::JNIEnv,
     env: *mut ndk_sys::JNIEnv,
     _: ndk_sys::jobject,
     _: ndk_sys::jobject,
+    id: ndk_sys::jint,
     start: ndk_sys::jint,
     start: ndk_sys::jint,
     end: ndk_sys::jint,
     end: ndk_sys::jint,
 ) {
 ) {
-    let begin = std::cmp::min(start, end);
-    let end = std::cmp::max(start, end);
-
-    if begin < 0 || end < 0 {
-        warn!(target: "android", "setComposeRegion({start}, {end}) is < 0 so skipping");
-        return
+    assert!(id >= 0);
+    let id = id as usize;
+    if let Some(sender) = &GLOBALS.lock().unwrap().sender {
+        let _ = sender.try_send(AndroidSuggestEvent::ComposeRegion {
+            start: start as usize,
+            end: end as usize,
+        });
     }
     }
-
-    let start = begin as usize;
-    let end = end as usize;
-
+}
+#[no_mangle]
+pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_onFinishCompose(
+    env: *mut ndk_sys::JNIEnv,
+    _: ndk_sys::jobject,
+    id: ndk_sys::jint,
+) {
+    assert!(id >= 0);
+    let id = id as usize;
     if let Some(sender) = &GLOBALS.lock().unwrap().sender {
     if let Some(sender) = &GLOBALS.lock().unwrap().sender {
-        let _ = sender.try_send(AndroidSuggestEvent::ComposeRegion { start, end });
+        let _ = sender.try_send(AndroidSuggestEvent::FinishCompose);
     }
     }
 }
 }
-
-pub fn set_sender(sender: async_channel::Sender<AndroidSuggestEvent>) {
-    GLOBALS.lock().unwrap().sender = Some(sender);
+#[no_mangle]
+pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_onDeleteSurroundingText(
+    env: *mut ndk_sys::JNIEnv,
+    _: ndk_sys::jobject,
+    id: ndk_sys::jint,
+    left: ndk_sys::jint,
+    right: ndk_sys::jint,
+) {
+    assert!(id >= 0);
+    let id = id as usize;
+    if let Some(sender) = &GLOBALS.lock().unwrap().sender {
+        let _ = sender.try_send(AndroidSuggestEvent::DeleteSurroundingText {
+            left: left as usize,
+            right: right as usize,
+        });
+    }
 }
 }
 
 
-pub fn cancel_composition() {
+pub fn create_composer(sender: async_channel::Sender<AndroidSuggestEvent>) -> usize {
+    let composer_id = {
+        let mut globals = GLOBALS.lock().unwrap();
+        let id = globals.next_id;
+        globals.next_id += 1;
+        globals.sender = Some(sender);
+        id
+    };
     unsafe {
     unsafe {
         let env = android::attach_jni_env();
         let env = android::attach_jni_env();
-
-        ndk_utils::call_void_method!(env, android::ACTIVITY, "cancelComposition", "()V");
+        ndk_utils::call_void_method!(env, android::ACTIVITY, "createComposer", "(I)V", composer_id);
     }
     }
+    composer_id
 }
 }
 
 
-pub fn get_keyboard_height() -> usize {
-    unsafe {
+pub fn focus(id: usize) -> Option<()> {
+    let is_success = unsafe {
         let env = android::attach_jni_env();
         let env = android::attach_jni_env();
 
 
-        ndk_utils::call_int_method!(env, android::ACTIVITY, "getKeyboardHeight", "()I") as usize
+        ndk_utils::call_bool_method!(env, android::ACTIVITY, "focus", "(I)Z", id as i32)
+    };
+    if is_success == 0u8 {
+        None
+    } else {
+        Some(())
     }
     }
 }
 }
 
 
-pub fn get_appdata_path() -> PathBuf {
-    let path = unsafe {
+pub fn set_text(id: usize, text: &str) -> Option<()> {
+    let ctext = std::ffi::CString::new(text).unwrap();
+    let is_success = unsafe {
         let env = android::attach_jni_env();
         let env = android::attach_jni_env();
 
 
-        let text = ndk_utils::call_object_method!(
+        let new_string_utf = (**env).NewStringUTF.unwrap();
+        let jtext = new_string_utf(env, ctext.as_ptr());
+
+        ndk_utils::call_bool_method!(
             env,
             env,
             android::ACTIVITY,
             android::ACTIVITY,
-            "getAppDataPath",
-            "()Ljava/lang/String;"
-        );
-        ndk_utils::get_utf_str!(env, text).to_string()
+            "setText",
+            "(ILjava/lang/String;)Z",
+            id as i32,
+            jtext
+        )
     };
     };
-    path.into()
+    if is_success == 0u8 {
+        None
+    } else {
+        Some(())
+    }
 }
 }
-pub fn get_external_storage_path() -> PathBuf {
-    let path = unsafe {
+
+pub fn set_selection(id: usize, select_start: usize, select_end: usize) -> Option<()> {
+    //trace!(target: "android", "set_selection({id}, {select_start}, {select_end})");
+    unsafe {
         let env = android::attach_jni_env();
         let env = android::attach_jni_env();
+        let input_connect = ndk_utils::call_object_method!(
+            env,
+            android::ACTIVITY,
+            "getInputConnect",
+            "(I)Lautosuggest/CustomInputConnection;",
+            id as i32
+        );
+        if input_connect.is_null() {
+            return None
+        }
 
 
-        let text = ndk_utils::call_object_method!(
+        ndk_utils::call_bool_method!(env, input_connect, "beginBatchEdit", "()Z");
+        ndk_utils::call_bool_method!(
+            env,
+            input_connect,
+            "setSelection",
+            "(II)Z",
+            select_start,
+            select_end
+        );
+        ndk_utils::call_bool_method!(env, input_connect, "endBatchEdit", "()Z");
+    }
+    Some(())
+}
+
+pub struct Editable {
+    pub buffer: String,
+    pub select_start: usize,
+    pub select_end: usize,
+    pub compose_start: Option<usize>,
+    pub compose_end: Option<usize>,
+}
+
+pub fn get_editable(id: usize) -> Option<Editable> {
+    //trace!(target: "android", "get_editable({id})");
+    unsafe {
+        let env = android::attach_jni_env();
+        let input_connect = ndk_utils::call_object_method!(
             env,
             env,
             android::ACTIVITY,
             android::ACTIVITY,
-            "getExternalStoragePath",
-            "()Ljava/lang/String;"
+            "getInputConnect",
+            "(I)Lautosuggest/CustomInputConnection;",
+            id as i32
         );
         );
-        ndk_utils::get_utf_str!(env, text).to_string()
-    };
-    path.into()
+        if input_connect.is_null() {
+            return None
+        }
+
+        let buffer =
+            ndk_utils::call_object_method!(env, input_connect, "rawText", "()Ljava/lang/String;");
+        let buffer = ndk_utils::get_utf_str!(env, buffer).to_string();
+
+        let select_start =
+            ndk_utils::call_int_method!(env, input_connect, "getSelectionStart", "()I");
+
+        let select_end = ndk_utils::call_int_method!(env, input_connect, "getSelectionEnd", "()I");
+
+        let compose_start =
+            ndk_utils::call_int_method!(env, input_connect, "getComposeStart", "()I");
+
+        let compose_end = ndk_utils::call_int_method!(env, input_connect, "getComposeEnd", "()I");
+
+        assert!(select_start >= 0);
+        assert!(select_end >= 0);
+        assert!(compose_start >= 0 || compose_start == compose_end);
+        assert!(compose_start <= compose_end);
+
+        Some(Editable {
+            buffer,
+            select_start: select_start as usize,
+            select_end: select_end as usize,
+            compose_start: if compose_start < 0 { None } else { Some(compose_start as usize) },
+            compose_end: if compose_end < 0 { None } else { Some(compose_end as usize) },
+        })
+    }
+}
+
+pub fn get_keyboard_height() -> usize {
+    call_mainactivity_int_method!("getKeyboardHeight", "()I") as usize
+}
+
+pub fn get_appdata_path() -> PathBuf {
+    call_mainactivity_str_method!("getAppDataPath").into()
+}
+pub fn get_external_storage_path() -> PathBuf {
+    call_mainactivity_str_method!("getExternalStoragePath").into()
 }
 }

+ 13 - 8
bin/app/src/app/node.rs

@@ -314,17 +314,14 @@ pub fn create_chatedit(name: &str) -> SceneNode {
     prop.set_ui_text("Is Focused", "A focused EditBox receives input");
     prop.set_ui_text("Is Focused", "A focused EditBox receives input");
     node.add_property(prop).unwrap();
     node.add_property(prop).unwrap();
 
 
-    let mut prop = Property::new("min_height", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_ui_text("Min Height", "Minimum height");
-    prop.set_range_f32(0., f32::MAX);
-    node.add_property(prop).unwrap();
-
-    let mut prop = Property::new("max_height", PropertyType::Float32, PropertySubType::Pixel);
-    prop.set_ui_text("Max Height", "Maximum height");
+    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_range_f32(0., f32::MAX);
+    prop.set_array_len(2);
     node.add_property(prop).unwrap();
     node.add_property(prop).unwrap();
 
 
-    let prop = Property::new("height", PropertyType::Float32, PropertySubType::Pixel);
+    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();
     node.add_property(prop).unwrap();
 
 
     let mut prop = Property::new("rect", PropertyType::Float32, PropertySubType::Pixel);
     let mut prop = Property::new("rect", PropertyType::Float32, PropertySubType::Pixel);
@@ -332,6 +329,7 @@ pub fn create_chatedit(name: &str) -> SceneNode {
     prop.allow_exprs();
     prop.allow_exprs();
     node.add_property(prop).unwrap();
     node.add_property(prop).unwrap();
 
 
+    // DEPRECATED ------------
     let prop = Property::new("baseline", PropertyType::Float32, PropertySubType::Pixel);
     let prop = Property::new("baseline", PropertyType::Float32, PropertySubType::Pixel);
     node.add_property(prop).unwrap();
     node.add_property(prop).unwrap();
 
 
@@ -340,6 +338,13 @@ pub fn create_chatedit(name: &str) -> SceneNode {
 
 
     let prop = Property::new("descent", PropertyType::Float32, PropertySubType::Pixel);
     let prop = Property::new("descent", PropertyType::Float32, PropertySubType::Pixel);
     node.add_property(prop).unwrap();
     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);
     let mut prop = Property::new("scroll", PropertyType::Float32, PropertySubType::Pixel);
     prop.set_range_f32(0., f32::MAX);
     prop.set_range_f32(0., f32::MAX);

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

@@ -479,16 +479,19 @@ pub async fn make(app: &App, window: SceneNodePtr) {
         })
         })
         .await;
         .await;
     layer_node.clone().link(node);
     layer_node.clone().link(node);
+    */
 
 
     // Text edit
     // Text edit
     let node = create_chatedit("editz");
     let node = create_chatedit("editz");
     node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
     node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
     node.set_property_bool(atom, Role::App, "is_focused", true).unwrap();
     node.set_property_bool(atom, Role::App, "is_focused", true).unwrap();
 
 
-    node.set_property_f32(atom, Role::App, "max_height", 400.).unwrap();
+    let prop = node.get_property("height_range").unwrap();
+    prop.clone().set_f32(atom, Role::App, 0, 100.).unwrap();
+    prop.clone().set_f32(atom, Role::App, 1, 400.).unwrap();
 
 
     let prop = node.get_property("rect").unwrap();
     let prop = node.get_property("rect").unwrap();
-    prop.clone().set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.clone().set_f32(atom, Role::App, 0, 100.).unwrap();
     prop.clone().set_f32(atom, Role::App, 1, 300.).unwrap();
     prop.clone().set_f32(atom, Role::App, 1, 300.).unwrap();
     prop.clone().set_expr(atom, Role::App, 2, expr::load_var("parent_w")).unwrap();
     prop.clone().set_expr(atom, Role::App, 2, expr::load_var("parent_w")).unwrap();
     prop.clone().set_f32(atom, Role::App, 3, 50.).unwrap();
     prop.clone().set_f32(atom, Role::App, 3, 50.).unwrap();
@@ -536,5 +539,4 @@ pub async fn make(app: &App, window: SceneNodePtr) {
         })
         })
         .await;
         .await;
     layer_node.clone().link(node);
     layer_node.clone().link(node);
-    */
 }
 }

+ 1 - 1
bin/app/src/logger.rs

@@ -212,7 +212,7 @@ pub fn setup_logging() {
     #[cfg(target_os = "android")]
     #[cfg(target_os = "android")]
     {
     {
         use android::AndroidLoggerWrapper;
         use android::AndroidLoggerWrapper;
-        let android_logger = AndroidLoggerWrapper::new(LevelFilter::Debug, cfg);
+        let android_logger = AndroidLoggerWrapper::new(LevelFilter::Trace, cfg);
         loggers.push(android_logger);
         loggers.push(android_logger);
     }
     }
 
 

+ 11 - 0
bin/app/src/main.rs

@@ -49,6 +49,16 @@ extern crate log;
 #[allow(unused_imports)]
 #[allow(unused_imports)]
 use log::LevelFilter;
 use log::LevelFilter;
 
 
+#[derive(Debug)]
+pub enum AndroidSuggestEvent {
+    Init,
+    CreateInputConnect,
+    Compose { text: String, cursor_pos: i32, is_commit: bool },
+    ComposeRegion { start: usize, end: usize },
+    FinishCompose,
+    DeleteSurroundingText { left: usize, right: usize },
+}
+
 #[cfg(target_os = "android")]
 #[cfg(target_os = "android")]
 mod android;
 mod android;
 mod app;
 mod app;
@@ -68,6 +78,7 @@ mod scene;
 mod shape;
 mod shape;
 use scene::SceneNode as SceneNode3;
 use scene::SceneNode as SceneNode3;
 mod text;
 mod text;
+mod text2;
 mod ui;
 mod ui;
 mod util;
 mod util;
 
 

+ 0 - 1
bin/app/src/text/mod.rs

@@ -35,7 +35,6 @@ use crate::gfx::Rectangle;
 
 
 mod atlas;
 mod atlas;
 pub use atlas::{make_texture_atlas, Atlas, RenderedAtlas};
 pub use atlas::{make_texture_atlas, Atlas, RenderedAtlas};
-pub mod atlas2;
 mod ft;
 mod ft;
 use ft::{render_glyph, FreetypeFace, Sprite, SpritePtr};
 use ft::{render_glyph, FreetypeFace, Sprite, SpritePtr};
 mod shape;
 mod shape;

+ 285 - 0
bin/app/src/text2/atlas.rs

@@ -0,0 +1,285 @@
+/* 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::{
+    error::Result,
+    gfx::{GfxTextureId, ManagedTexturePtr, Rectangle, RenderApi},
+    mesh::Color,
+};
+
+/// Prevents render artifacts from aliasing.
+/// Even with aliasing turned off, some bleed still appears possibly
+/// due to UV coord calcs. Adding a gap perfectly fixes this.
+const ATLAS_GAP: usize = 2;
+
+/*
+/// Convenience wrapper fn. Use if rendering a single line of glyphs.
+pub fn make_texture_atlas(render_api: &RenderApi, glyphs: &Vec<Glyph>) -> RenderedAtlas {
+    let mut atlas = Atlas::new(render_api);
+    atlas.push(&glyphs);
+    atlas.make()
+}
+*/
+
+//pub struct Sprite(swash::scale::image::Image);
+
+/// Responsible for aggregating glyphs, and then producing a single software
+/// blitted texture usable in a single draw call.
+/// This makes OpenGL batch precomputation of meshes efficient.
+///
+/// ```rust
+///     let mut atlas = Atlas::new(&render_api);
+///     atlas.push(&glyphs);    // repeat as needed for shaped lines
+///     let atlas = atlas.make().unwrap();
+///     let uv = atlas.fetch_uv(glyph_id).unwrap();
+///     let atlas_texture_id = atlas.texture_id;
+/// ```
+pub struct Atlas<'a> {
+    scaler: swash::scale::Scaler<'a>,
+    glyph_ids: Vec<swash::GlyphId>,
+    sprites: Vec<swash::scale::image::Image>,
+    // LHS x pos of glyph
+    x_pos: Vec<usize>,
+
+    width: usize,
+    height: usize,
+
+    render_api: &'a RenderApi,
+}
+
+impl<'a> Atlas<'a> {
+    pub fn new(scaler: swash::scale::Scaler<'a>, render_api: &'a RenderApi) -> Self {
+        Self {
+            scaler,
+            glyph_ids: vec![],
+            sprites: vec![],
+            x_pos: vec![],
+
+            width: ATLAS_GAP,
+            // Not really important to set a value here since it will
+            // get overwritten.
+            // FYI glyphs have a gap on all sides (top and bottom here).
+            height: 2 * ATLAS_GAP,
+
+            render_api,
+        }
+    }
+
+    pub fn push_glyph(&mut self, glyph: parley::Glyph) {
+        if self.glyph_ids.contains(&glyph.id) {
+            return
+        }
+
+        self.glyph_ids.push(glyph.id);
+
+        let rendered_glyph = swash::scale::Render::new(
+            // Select our source order
+            &[
+                swash::scale::Source::ColorOutline(0),
+                swash::scale::Source::ColorBitmap(swash::scale::StrikeWith::BestFit),
+                swash::scale::Source::Outline,
+            ],
+        )
+        // Select the simple alpha (non-subpixel) format
+        .format(zeno::Format::Alpha)
+        .render(&mut self.scaler, glyph.id)
+        .unwrap();
+
+        let glyph_width = rendered_glyph.placement.width as usize;
+        let glyph_height = rendered_glyph.placement.height as usize;
+
+        self.sprites.push(rendered_glyph);
+
+        self.x_pos.push(self.width);
+
+        // Gap on the top and bottom
+        let height = ATLAS_GAP + glyph_height + ATLAS_GAP;
+        self.height = std::cmp::max(height, self.height);
+
+        // Gap between glyphs and on both sides
+        self.width += glyph_width + ATLAS_GAP;
+    }
+
+    fn render(&self) -> Vec<u8> {
+        let mut atlas = vec![255, 255, 255, 0].repeat(self.width * self.height);
+        // For drawing debug lines we want a single white pixel.
+        // This is very useful to have in our texture for debugging.
+        atlas[0] = 255;
+        atlas[1] = 255;
+        atlas[2] = 255;
+        atlas[3] = 255;
+
+        let y = ATLAS_GAP;
+        // Copy all the sprites to our atlas.
+        // They should have ATLAS_GAP spacing on all sides to avoid bleeding.
+        for (sprite, x) in self.sprites.iter().zip(self.x_pos.iter()) {
+            copy_image(sprite, *x, y, &mut atlas, self.width);
+        }
+
+        atlas
+    }
+
+    fn compute_uvs(&self) -> Vec<Rectangle> {
+        // UV coords are in the range [0, 1]
+        let mut uvs = Vec::with_capacity(self.sprites.len());
+
+        let (self_w, self_h) = (self.width as f32, self.height as f32);
+        let y = ATLAS_GAP as f32;
+
+        for (sprite, x) in self.sprites.iter().zip(self.x_pos.iter()) {
+            let x = *x as f32;
+            let sprite_w = sprite.placement.width as f32;
+            let sprite_h = sprite.placement.height as f32;
+
+            let uv = Rectangle {
+                x: x / self_w,
+                y: y / self_h,
+                w: sprite_w / self_w,
+                h: sprite_h / self_h,
+            };
+            uvs.push(uv);
+        }
+
+        uvs
+    }
+
+    /// Debug method
+    pub fn dump(&self, output_path: &str) {
+        let atlas = self.render();
+        let img = image::RgbaImage::from_raw(self.width as u32, self.height as u32, atlas).unwrap();
+        img.save(output_path);
+    }
+
+    /// Invalidate this atlas and produce the finalized result.
+    /// Each glyph is given a sub-rect within the texture, accessible by calling
+    /// `rendered_atlas.fetch_uv(my_glyph_id)`.
+    /// The texture ID is a struct member: `rendered_atlas.texture_id`.
+    pub fn make(self) -> RenderedAtlas {
+        //if self.glyph_ids.is_empty() {
+        //    return Err(Error::AtlasIsEmpty);
+        //}
+
+        assert_eq!(self.glyph_ids.len(), self.sprites.len());
+        assert_eq!(self.glyph_ids.len(), self.x_pos.len());
+
+        let atlas = self.render();
+        let texture = self.render_api.new_texture(self.width as u16, self.height as u16, atlas);
+
+        let uv_rects = self.compute_uvs();
+        let glyph_ids = self.glyph_ids;
+
+        let mut infos = Vec::with_capacity(self.sprites.len());
+        for (uv_rect, sprite) in uv_rects.into_iter().zip(self.sprites.into_iter()) {
+            let is_color = match sprite.content {
+                swash::scale::image::Content::Mask => false,
+                swash::scale::image::Content::SubpixelMask => unimplemented!(),
+                swash::scale::image::Content::Color => true,
+            };
+            infos.push(GlyphInfo { uv_rect, place: sprite.placement, is_color });
+        }
+
+        RenderedAtlas { glyph_ids, infos, texture }
+    }
+}
+
+/// Copy a sprite to (x, y) position within the atlas texture.
+/// Both image formats are RGBA flat vecs.
+fn copy_image(
+    sprite: &swash::scale::image::Image,
+    x: usize,
+    y: usize,
+    atlas: &mut Vec<u8>,
+    atlas_width: usize,
+) {
+    let sprite_width = sprite.placement.width as usize;
+    let sprite_height = sprite.placement.height as usize;
+
+    match sprite.content {
+        swash::scale::image::Content::Mask => {
+            let mut i = 0;
+            for pixel_y in 0..sprite_height {
+                for pixel_x in 0..sprite_width {
+                    let src_alpha = sprite.data[i];
+
+                    let dest_y = (y + pixel_y) * atlas_width;
+                    let off_dest = 4 * (dest_y + pixel_x + x);
+
+                    //atlas[off_dest] = 255;
+                    //atlas[off_dest + 1] = 255;
+                    //atlas[off_dest + 2] = 255;
+                    atlas[off_dest + 3] = src_alpha;
+
+                    i += 1;
+                }
+            }
+        }
+        swash::scale::image::Content::SubpixelMask => unimplemented!(),
+        swash::scale::image::Content::Color => {
+            let row_size = sprite_width * 4;
+            for (pixel_y, row) in sprite.data.chunks_exact(row_size).enumerate() {
+                for (pixel_x, pixel) in row.chunks_exact(4).enumerate() {
+                    assert_eq!(pixel.len(), 4);
+
+                    let src_y = pixel_y * sprite_width;
+                    let off_src = 4 * (src_y + pixel_x);
+
+                    let dest_y = (y + pixel_y) * atlas_width;
+                    let off_dest = 4 * (dest_y + pixel_x + x);
+
+                    atlas[off_dest] = pixel[0];
+                    atlas[off_dest + 1] = pixel[1];
+                    atlas[off_dest + 2] = pixel[2];
+                    atlas[off_dest + 3] = pixel[3];
+                }
+            }
+        }
+    }
+}
+
+#[derive(Clone)]
+pub struct GlyphInfo {
+    /// UV rectangle within the texture.
+    pub uv_rect: Rectangle,
+    /// Placement of the sprite used to calc the rect
+    pub place: zeno::Placement,
+    pub is_color: bool,
+}
+
+/// Final result computed from `Atlas::make()`.
+#[derive(Clone)]
+pub struct RenderedAtlas {
+    glyph_ids: Vec<swash::GlyphId>,
+    infos: Vec<GlyphInfo>,
+    /// Allocated atlas texture.
+    pub texture: ManagedTexturePtr,
+}
+
+impl RenderedAtlas {
+    /// Get UV coords for a glyph within the rendered atlas.
+    pub fn fetch_uv(&self, glyph_id: swash::GlyphId) -> Option<&GlyphInfo> {
+        let glyphs_len = self.glyph_ids.len();
+        assert_eq!(glyphs_len, self.infos.len());
+
+        for i in 0..glyphs_len {
+            if self.glyph_ids[i] == glyph_id {
+                return Some(&self.infos[i])
+            }
+        }
+        None
+    }
+}

+ 152 - 0
bin/app/src/text2/editor/android.rs

@@ -0,0 +1,152 @@
+/* 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::{
+    android,
+    gfx::Point,
+    mesh::Color,
+    prop::{PropertyColor, PropertyFloat32},
+    text2::get_ctx,
+};
+
+macro_rules! t { ($($arg:tt)*) => { trace!(target: "text::editor::android", $($arg)*); } }
+
+// You must be careful working with string indexes in Java. They are UTF16 string indexs, not UTF8
+fn char16_to_byte_index(s: &str, char_idx: usize) -> Option<usize> {
+    let utf16_data: Vec<_> = s.encode_utf16().take(char_idx).collect();
+    let prestr = String::from_utf16(&utf16_data).ok()?;
+    Some(prestr.len())
+}
+fn byte_to_char16_index(s: &str, byte_idx: usize) -> Option<usize> {
+    if byte_idx > s.len() || !s.is_char_boundary(byte_idx) {
+        return None;
+    }
+    Some(s[..byte_idx].encode_utf16().count())
+}
+
+pub struct Editor {
+    pub composer_id: usize,
+    is_init: bool,
+
+    layout: parley::Layout<Color>,
+
+    font_size: PropertyFloat32,
+    text_color: PropertyColor,
+    window_scale: PropertyFloat32,
+    lineheight: PropertyFloat32,
+}
+
+impl Editor {
+    pub async fn new(
+        font_size: PropertyFloat32,
+        text_color: PropertyColor,
+        window_scale: PropertyFloat32,
+        lineheight: PropertyFloat32,
+    ) -> Self {
+        Self {
+            composer_id: usize::MAX,
+            is_init: false,
+
+            layout: Default::default(),
+            font_size,
+            text_color,
+            window_scale,
+            lineheight,
+        }
+    }
+
+    pub fn init(&mut self) {
+        android::focus(self.composer_id).unwrap();
+    }
+    pub fn setup(&mut self) {
+        assert!(self.composer_id != usize::MAX);
+        t!("Initialized composer [{}]", self.composer_id);
+        let atxt = "A berry is small 😊 and pulpy.";
+        android::set_text(self.composer_id, atxt).unwrap();
+        self.is_init = true;
+    }
+
+    pub async fn refresh(&mut self) {
+        let font_size = self.font_size.get();
+        let text_color = self.text_color.get();
+        let window_scale = self.window_scale.get();
+        let lineheight = self.lineheight.get();
+
+        let edit = android::get_editable(self.composer_id).unwrap();
+        t!("refesh buffer = {}", edit.buffer);
+
+        let mut txt_ctx = get_ctx().await;
+        self.layout = txt_ctx.make_layout(
+            &edit.buffer,
+            text_color,
+            font_size,
+            lineheight,
+            window_scale,
+            None,
+        );
+    }
+
+    pub fn layout(&self) -> &parley::Layout<Color> {
+        &self.layout
+    }
+
+    pub fn move_to_pos(&self, pos: Point) {
+        let cursor = parley::Cursor::from_point(&self.layout, pos.x, pos.y);
+
+        let edit = android::get_editable(self.composer_id).unwrap();
+        let cursor_idx = cursor.index();
+        let pos = byte_to_char16_index(&edit.buffer, cursor_idx).unwrap();
+        android::set_selection(self.composer_id, pos, pos);
+        t!("  {cursor_idx} => {pos}");
+    }
+
+    pub fn get_cursor_pos(&self) -> Option<Point> {
+        if !self.is_init {
+            return None
+        }
+
+        let lineheight = self.lineheight.get();
+        let edit = android::get_editable(self.composer_id).unwrap();
+
+        //let buffer = android::get_raw_text(self.composer_id).unwrap();
+        //let sel_start = android::get_selection_start(self.composer_id).unwrap();
+        //let sel_end = android::get_selection_end(self.composer_id).unwrap();
+        //if sel_start != sel_end || sel_start < 0 {
+        //    return None
+        //}
+        let cursor_byte_idx = char16_to_byte_index(&edit.buffer, edit.select_start).unwrap();
+
+        let cursor = if cursor_byte_idx >= edit.buffer.len() {
+            parley::Cursor::from_byte_index(
+                &self.layout,
+                edit.buffer.len(),
+                parley::Affinity::Upstream,
+            )
+        } else {
+            parley::Cursor::from_byte_index(
+                &self.layout,
+                cursor_byte_idx,
+                parley::Affinity::Downstream,
+            )
+        };
+        let cursor_rect = cursor.geometry(&self.layout, lineheight);
+
+        let cursor_pos = Point::new(cursor_rect.x0 as f32, cursor_rect.y0 as f32);
+        Some(cursor_pos)
+    }
+}

+ 27 - 0
bin/app/src/text2/editor/mod.rs

@@ -0,0 +1,27 @@
+/* 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/>.
+ */
+
+#[cfg(target_os = "android")]
+mod android;
+#[cfg(target_os = "android")]
+pub use android::Editor;
+
+#[cfg(not(target_os = "android"))]
+mod parley;
+#[cfg(not(target_os = "android"))]
+pub use parley::Editor;

+ 118 - 0
bin/app/src/text2/editor/parley.rs

@@ -0,0 +1,118 @@
+/* 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::{
+    gfx::Point,
+    mesh::Color,
+    prop::{PropertyColor, PropertyFloat32},
+    text2::{get_ctx, TextContext, FONT_STACK},
+};
+
+macro_rules! t { ($($arg:tt)*) => { trace!(target: "text::editor", $($arg)*); } }
+
+pub struct Editor {
+    editor: parley::PlainEditor<Color>,
+
+    font_size: PropertyFloat32,
+    text_color: PropertyColor,
+    window_scale: PropertyFloat32,
+    lineheight: PropertyFloat32,
+}
+
+impl Editor {
+    pub async fn new(
+        font_size: PropertyFloat32,
+        text_color: PropertyColor,
+        window_scale: PropertyFloat32,
+        lineheight: PropertyFloat32,
+    ) -> Self {
+        let editor = parley::PlainEditor::new(1.);
+        let mut self_ = Self { editor, font_size, text_color, window_scale, lineheight };
+        self_.refresh().await;
+        self_
+    }
+
+    pub fn init(&mut self) {}
+    pub fn setup(&mut self) {}
+
+    pub async fn refresh(&mut self) {
+        let font_size = self.font_size.get();
+        let text_color = self.text_color.get();
+        let window_scale = self.window_scale.get();
+        let lineheight = self.lineheight.get();
+
+        self.editor.set_scale(window_scale);
+        let mut styles = parley::StyleSet::new(font_size);
+        styles.insert(parley::StyleProperty::LineHeight(lineheight));
+        styles.insert(parley::StyleProperty::FontStack(parley::FontStack::List(FONT_STACK.into())));
+        styles.insert(parley::StyleProperty::Brush(text_color));
+        *self.editor.edit_styles() = styles;
+
+        let mut txt_ctx = get_ctx().await;
+        let (font_ctx, layout_ctx) = txt_ctx.borrow();
+        self.editor.refresh_layout(font_ctx, layout_ctx);
+    }
+
+    pub fn layout(&self) -> &parley::Layout<Color> {
+        self.editor.try_layout().unwrap()
+    }
+
+    pub fn move_to_pos(&self, pos: Point) {}
+
+    pub fn get_cursor_pos(&self) -> Option<Point> {
+        let lineheight = self.lineheight.get();
+        let cursor_rect = self.editor.cursor_geometry(lineheight).unwrap();
+        let cursor_pos = Point::new(cursor_rect.x0 as f32, cursor_rect.y0 as f32);
+        Some(cursor_pos)
+    }
+
+    pub async fn driver<'a>(&'a mut self) -> Option<DriverWrapper<'a>> {
+        let mut txt_ctx = get_ctx().await;
+        // I'm one billion percent sure this is safe and don't want to waste time
+        let (font_ctx, layout_ctx) = {
+            let (f, l) = txt_ctx.borrow();
+            let f: &'a mut parley::FontContext = unsafe { std::mem::transmute(f) };
+            let l: &'a mut parley::LayoutContext<Color> = unsafe { std::mem::transmute(l) };
+            (f, l)
+        };
+        let drv = self.editor.driver(font_ctx, layout_ctx);
+        // Storing the MutexGuard together with its dependent value drv ensures we cannot
+        // have a race condition and the lifetime rules are respected.
+        let drv = DriverWrapper { txt_ctx, drv };
+        Some(drv)
+    }
+}
+
+pub struct DriverWrapper<'a> {
+    txt_ctx: async_lock::MutexGuard<'static, TextContext>,
+    drv: parley::PlainEditorDriver<'a, Color>,
+}
+
+impl<'a> std::ops::Deref for DriverWrapper<'a> {
+    type Target = parley::PlainEditorDriver<'a, Color>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.drv
+    }
+}
+
+impl<'a> std::ops::DerefMut for DriverWrapper<'a> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.drv
+    }
+}

+ 92 - 0
bin/app/src/text2/mod.rs

@@ -0,0 +1,92 @@
+/* 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 std::sync::{Arc, OnceLock};
+
+use crate::mesh::Color;
+
+pub mod atlas;
+mod editor;
+pub use editor::Editor;
+mod render;
+pub use render::{render_layout, DebugRenderOptions};
+
+static TEXT_CTX: OnceLock<AsyncMutex<TextContext>> = OnceLock::new();
+
+pub async fn get_ctx() -> async_lock::MutexGuard<'static, TextContext> {
+    TEXT_CTX.get_or_init(|| AsyncMutex::new(TextContext::new())).lock().await
+}
+
+pub struct TextContext {
+    font_ctx: parley::FontContext,
+    layout_ctx: parley::LayoutContext<Color>,
+}
+
+impl TextContext {
+    fn new() -> Self {
+        let mut font_ctx = parley::FontContext::new();
+
+        let font_data = include_bytes!("../../ibm-plex-mono-regular.otf") as &[u8];
+        let font_inf =
+            font_ctx.collection.register_fonts(peniko::Blob::new(Arc::new(font_data)), None);
+
+        let font_data = include_bytes!("../../NotoColorEmoji.ttf") as &[u8];
+        let font_inf =
+            font_ctx.collection.register_fonts(peniko::Blob::new(Arc::new(font_data)), None);
+
+        for (family_id, _) in font_inf {
+            let family_name = font_ctx.collection.family_name(family_id).unwrap();
+            trace!(target: "text", "Loaded font: {family_name}");
+        }
+
+        Self { font_ctx, layout_ctx: Default::default() }
+    }
+
+    pub fn borrow(&mut self) -> (&mut parley::FontContext, &mut parley::LayoutContext<Color>) {
+        (&mut self.font_ctx, &mut self.layout_ctx)
+    }
+
+    pub fn make_layout(
+        &mut self,
+        text: &str,
+        text_color: Color,
+        font_size: f32,
+        lineheight: f32,
+        window_scale: f32,
+        width: Option<f32>,
+    ) -> parley::Layout<Color> {
+        let mut builder = self.layout_ctx.ranged_builder(&mut self.font_ctx, &text, window_scale);
+        builder.push_default(parley::StyleProperty::LineHeight(lineheight));
+        builder.push_default(parley::StyleProperty::FontSize(font_size));
+        builder.push_default(parley::StyleProperty::FontStack(parley::FontStack::List(
+            FONT_STACK.into(),
+        )));
+        builder.push_default(parley::StyleProperty::Brush(text_color));
+
+        let mut layout: parley::Layout<Color> = builder.build(&text);
+        layout.break_all_lines(width);
+        layout.align(width, parley::Alignment::Start, parley::AlignmentOptions::default());
+        layout
+    }
+}
+
+pub const FONT_STACK: &[parley::FontFamily<'_>] = &[
+    parley::FontFamily::Named(std::borrow::Cow::Borrowed("IBM Plex Mono")),
+    parley::FontFamily::Named(std::borrow::Cow::Borrowed("Noto Color Emoji")),
+];

+ 143 - 0
bin/app/src/text2/render.rs

@@ -0,0 +1,143 @@
+/* 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::{
+    gfx::{GfxDrawInstruction, GfxDrawMesh, Rectangle, RenderApi},
+    mesh::{Color, MeshBuilder, COLOR_WHITE},
+};
+
+use super::atlas::Atlas;
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub struct DebugRenderOptions(u32);
+
+impl DebugRenderOptions {
+    pub const Off: DebugRenderOptions = DebugRenderOptions(0b00);
+    pub const Glyph: DebugRenderOptions = DebugRenderOptions(0b01);
+    pub const Baseline: DebugRenderOptions = DebugRenderOptions(0b10);
+
+    pub fn has(self, other: Self) -> bool {
+        (self.0 & other.0) == other.0
+    }
+}
+
+impl std::ops::BitOr for DebugRenderOptions {
+    type Output = Self;
+
+    fn bitor(self, rhs: Self) -> Self {
+        Self(self.0 | rhs.0)
+    }
+}
+
+pub fn render_layout(
+    layout: &parley::Layout<Color>,
+    render_api: &RenderApi,
+) -> Vec<GfxDrawInstruction> {
+    render_layout_with_opts(layout, DebugRenderOptions::Off, render_api)
+}
+
+pub fn render_layout_with_opts(
+    layout: &parley::Layout<Color>,
+    opts: DebugRenderOptions,
+    render_api: &RenderApi,
+) -> Vec<GfxDrawInstruction> {
+    let mut scale_cx = swash::scale::ScaleContext::new();
+    let mut run_idx = 0;
+    let mut instrs = vec![];
+    for line in layout.lines() {
+        for item in line.items() {
+            match item {
+                parley::PositionedLayoutItem::GlyphRun(glyph_run) => {
+                    let mesh =
+                        render_glyph_run(&mut scale_cx, &glyph_run, run_idx, opts, render_api);
+                    instrs.push(GfxDrawInstruction::Draw(mesh));
+                    run_idx += 1;
+                }
+                parley::PositionedLayoutItem::InlineBox(_) => {}
+            }
+        }
+    }
+    instrs
+}
+
+fn render_glyph_run(
+    scale_ctx: &mut swash::scale::ScaleContext,
+    glyph_run: &parley::GlyphRun<'_, Color>,
+    run_idx: usize,
+    opts: DebugRenderOptions,
+    render_api: &RenderApi,
+) -> GfxDrawMesh {
+    let mut run_x = glyph_run.offset();
+    let run_y = glyph_run.baseline();
+    let style = glyph_run.style();
+    let color = style.brush;
+
+    let run = glyph_run.run();
+    trace!(target: "text::render", "render_glyph_run run_idx={run_idx}");
+
+    let font = run.font();
+    let font_size = run.font_size();
+    let normalized_coords = run.normalized_coords();
+    let font_ref = swash::FontRef::from_index(font.data.as_ref(), font.index as usize).unwrap();
+
+    let mut scaler = scale_ctx
+        .builder(font_ref)
+        .size(font_size)
+        .hint(true)
+        .normalized_coords(normalized_coords)
+        .build();
+
+    let mut atlas = Atlas::new(scaler, render_api);
+    for glyph in glyph_run.glyphs() {
+        atlas.push_glyph(glyph);
+    }
+    //atlas.dump(&format!("/tmp/atlas_{run_idx}.png"));
+    let atlas = atlas.make();
+
+    let mut mesh = MeshBuilder::new();
+    for glyph in glyph_run.glyphs() {
+        let glyph_inf = atlas.fetch_uv(glyph.id).expect("missing glyph UV rect");
+
+        let glyph_x = run_x + glyph.x;
+        let glyph_y = run_y - glyph.y;
+        run_x += glyph.advance;
+
+        let glyph_rect = Rectangle::new(
+            glyph_x + glyph_inf.place.left as f32,
+            glyph_y - glyph_inf.place.top as f32,
+            glyph_inf.place.width as f32,
+            glyph_inf.place.height as f32,
+        );
+
+        if opts.has(DebugRenderOptions::Glyph) {
+            mesh.draw_outline(&glyph_rect, [0., 1., 0., 0.7], 1.);
+        }
+
+        let color = if glyph_inf.is_color { COLOR_WHITE } else { color };
+        mesh.draw_box(&glyph_rect, color, &glyph_inf.uv_rect);
+    }
+
+    if opts.has(DebugRenderOptions::Baseline) {
+        mesh.draw_filled_box(
+            &Rectangle::new(glyph_run.offset(), glyph_run.baseline(), glyph_run.advance(), 1.),
+            [0., 0., 1., 0.7],
+        );
+    }
+
+    mesh.alloc(render_api).draw_with_texture(atlas.texture)
+}

Разница между файлами не показана из-за своего большого размера
+ 108 - 700
bin/app/src/ui/chatedit.rs


+ 2 - 2
bin/app/src/ui/editbox/editable.rs

@@ -231,8 +231,8 @@ impl Editable {
 
 
     /// Reset any composition in progress
     /// Reset any composition in progress
     pub fn end_compose(&mut self) {
     pub fn end_compose(&mut self) {
-        #[cfg(target_os = "android")]
-        crate::android::cancel_composition();
+        //#[cfg(target_os = "android")]
+        //crate::android::cancel_composition();
 
 
         //debug!(target: "ui::editbox", "end_compose() [editable={self:?}]");
         //debug!(target: "ui::editbox", "end_compose() [editable={self:?}]");
         let final_text = self.composer.clear();
         let final_text = self.composer.clear();

+ 8 - 1
bin/app/src/ui/editbox/mod.rs

@@ -1558,7 +1558,13 @@ impl UIObject for EditBox {
         }
         }
     }
     }
 
 
-    async fn handle_compose_text(&self, suggest_text: &str, is_commit: bool) -> bool {
+    /*
+    async fn handle_compose_text(
+        &self,
+        suggest_text: &str,
+        cursor_pos: i32,
+        is_commit: bool,
+    ) -> bool {
         t!("handle_compose_text({suggest_text}, {is_commit})");
         t!("handle_compose_text({suggest_text}, {is_commit})");
 
 
         if !self.is_active.get() {
         if !self.is_active.get() {
@@ -1593,6 +1599,7 @@ impl UIObject for EditBox {
 
 
         true
         true
     }
     }
+    */
 }
 }
 
 
 /// Filter these char events from being handled since we handle them
 /// Filter these char events from being handled since we handle them

+ 1 - 25
bin/app/src/ui/layer.rs

@@ -31,7 +31,7 @@ use crate::{
     },
     },
     scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
     scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
     util::unixtime,
     util::unixtime,
-    ExecutorPtr,
+    AndroidSuggestEvent, ExecutorPtr,
 };
 };
 
 
 use super::{
 use super::{
@@ -305,28 +305,4 @@ impl UIObject for Layer {
         }
         }
         false
         false
     }
     }
-    async fn handle_compose_text(&self, suggest_text: &str, is_commit: bool) -> bool {
-        if !self.is_visible.get() {
-            return false
-        }
-        for child in self.get_children() {
-            let obj = get_ui_object3(&child);
-            if obj.handle_compose_text(suggest_text, is_commit).await {
-                return true
-            }
-        }
-        false
-    }
-    async fn handle_set_compose_region(&self, start: usize, end: usize) -> bool {
-        if !self.is_visible.get() {
-            return false
-        }
-        for child in self.get_children() {
-            let obj = get_ui_object3(&child);
-            if obj.handle_set_compose_region(start, end).await {
-                return true
-            }
-        }
-        false
-    }
 }
 }

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

@@ -31,7 +31,7 @@ use crate::{
     gfx::{GfxBufferId, GfxDrawCall, GfxDrawMesh, GfxTextureId, Point, Rectangle},
     gfx::{GfxBufferId, GfxDrawCall, GfxDrawMesh, GfxTextureId, Point, Rectangle},
     prop::{ModifyAction, PropertyAtomicGuard, PropertyPtr, Role},
     prop::{ModifyAction, PropertyAtomicGuard, PropertyPtr, Role},
     scene::{Pimpl, SceneNode as SceneNode3, SceneNodeId, SceneNodePtr, SceneNodeWeak},
     scene::{Pimpl, SceneNode as SceneNode3, SceneNodeId, SceneNodePtr, SceneNodeWeak},
-    ExecutorPtr,
+    AndroidSuggestEvent, ExecutorPtr,
 };
 };
 
 
 mod button;
 mod button;
@@ -104,14 +104,6 @@ pub trait UIObject: Sync {
     async fn handle_touch(&self, phase: TouchPhase, id: u64, touch_pos: Point) -> bool {
     async fn handle_touch(&self, phase: TouchPhase, id: u64, touch_pos: Point) -> bool {
         false
         false
     }
     }
-
-    // Android Autosuggest
-    async fn handle_compose_text(&self, text: &str, is_commit: bool) -> bool {
-        false
-    }
-    async fn handle_set_compose_region(&self, start: usize, end: usize) -> bool {
-        false
-    }
 }
 }
 
 
 pub struct DrawUpdate {
 pub struct DrawUpdate {

+ 8 - 142
bin/app/src/ui/text.rs

@@ -32,6 +32,7 @@ use crate::{
     },
     },
     scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
     scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
     text::{self, GlyphPositionIter, TextShaper, TextShaperPtr},
     text::{self, GlyphPositionIter, TextShaper, TextShaperPtr},
+    text2,
     util::unixtime,
     util::unixtime,
     ExecutorPtr,
     ExecutorPtr,
 };
 };
@@ -116,143 +117,18 @@ impl Text {
         Pimpl::Text(self_)
         Pimpl::Text(self_)
     }
     }
 
 
-    fn regen_mesh2(&self) -> Vec<GfxDrawInstruction> {
+    async fn regen_mesh(&self) -> Vec<GfxDrawInstruction> {
         let text = self.text.get();
         let text = self.text.get();
         let font_size = self.font_size.get();
         let font_size = self.font_size.get();
         let text_color = self.text_color.get();
         let text_color = self.text_color.get();
         let window_scale = self.window_scale.get();
         let window_scale = self.window_scale.get();
 
 
-        let mut layout_cx = parley::LayoutContext::new();
-        let mut font_cx = parley::FontContext::new();
-
-        let mut builder = layout_cx.ranged_builder(&mut font_cx, &text, window_scale);
-
-        let brush_style = parley::StyleProperty::Brush(text_color);
-        builder.push_default(brush_style);
-
-        let font_stack = parley::FontStack::from("system-ui");
-        builder.push_default(font_stack);
-        builder.push_default(parley::StyleProperty::LineHeight(2.));
-        builder.push_default(parley::StyleProperty::FontSize(font_size));
-
-        let mut layout: parley::Layout<Color> = builder.build(&text);
-        // Perform layout (including bidi resolution and shaping) with start alignment
-        layout.break_all_lines(None);
-        layout.align(None, parley::Alignment::Start, parley::AlignmentOptions::default());
-
-        let mut scale_cx = swash::scale::ScaleContext::new();
-        let mut run_idx = 0;
-        let mut instrs = vec![];
-        for line in layout.lines() {
-            for item in line.items() {
-                match item {
-                    parley::PositionedLayoutItem::GlyphRun(glyph_run) => {
-                        let mesh = self.render_glyph_run(&mut scale_cx, &glyph_run, &text, run_idx);
-                        instrs.push(GfxDrawInstruction::Draw(mesh));
-                        run_idx += 1;
-                    }
-                    parley::PositionedLayoutItem::InlineBox(_) => {}
-                }
-            }
-        }
-        instrs
-    }
-
-    fn render_glyph_run(
-        &self,
-        scale_ctx: &mut swash::scale::ScaleContext,
-        glyph_run: &parley::GlyphRun<'_, Color>,
-        text: &str,
-        run_idx: usize,
-    ) -> GfxDrawMesh {
-        let mut run_x = glyph_run.offset();
-        let run_y = glyph_run.baseline();
-        let style = glyph_run.style();
-        let color = style.brush;
-
-        let run = glyph_run.run();
-        let text = &text[run.text_range()];
-        t!("render_glyph_run '{text}' run_idx={run_idx}");
-
-        let font = run.font();
-        let font_size = run.font_size();
-        let normalized_coords = run.normalized_coords();
-        let font_ref = swash::FontRef::from_index(font.data.as_ref(), font.index as usize).unwrap();
-
-        let mut scaler = scale_ctx
-            .builder(font_ref)
-            .size(font_size)
-            .hint(true)
-            .normalized_coords(normalized_coords)
-            .build();
-
-        let mut atlas = text::atlas2::Atlas::new(scaler, &self.render_api);
-        for glyph in glyph_run.glyphs() {
-            atlas.push_glyph(glyph);
-        }
-        atlas.dump(&format!("/tmp/atlas_{run_idx}.png"));
-        let atlas = atlas.make();
-
-        let mut mesh = MeshBuilder::new();
-        for glyph in glyph_run.glyphs() {
-            let glyph_inf = atlas.fetch_uv(glyph.id).expect("missing glyph UV rect");
-
-            let glyph_x = run_x + glyph.x;
-            let glyph_y = run_y - glyph.y;
-            run_x += glyph.advance;
-
-            let glyph_rect = Rectangle::new(
-                glyph_x + glyph_inf.place.left as f32,
-                glyph_y - glyph_inf.place.top as f32,
-                glyph_inf.place.width as f32,
-                glyph_inf.place.height as f32,
-            );
-
-            let color = if glyph_inf.is_color { COLOR_WHITE } else { color };
-
-            mesh.draw_box(&glyph_rect, color, &glyph_inf.uv_rect);
-        }
-        mesh.alloc(&self.render_api).draw_with_texture(atlas.texture)
-    }
-
-    fn regen_mesh(&self) -> TextRenderInfo {
-        let text = self.text.get();
-        let font_size = self.font_size.get();
-        let text_color = self.text_color.get();
-        let baseline = self.baseline.get();
-        let debug = self.debug.get();
-        let window_scale = self.window_scale.get();
-
-        t!("Rendering label '{}'", text);
-        let glyphs = self.text_shaper.shape(text, font_size, window_scale);
-        let atlas = text::make_texture_atlas(&self.render_api, &glyphs);
-
-        let mut mesh = MeshBuilder::new();
-        let glyph_pos_iter = GlyphPositionIter::new(font_size, window_scale, &glyphs, baseline);
-        for (mut glyph_rect, glyph) in glyph_pos_iter.zip(glyphs.iter()) {
-            let uv_rect = atlas.fetch_uv(glyph.glyph_id).expect("missing glyph UV rect");
-
-            if debug {
-                mesh.draw_outline(&glyph_rect, COLOR_BLUE, 2.);
-            }
-
-            let mut color = text_color.clone();
-            if glyph.sprite.has_color {
-                color = COLOR_WHITE;
-            }
-            mesh.draw_box(&glyph_rect, color, uv_rect);
-        }
-
-        if debug {
-            let mut rect = self.rect.get();
-            rect.x = 0.;
-            rect.y = 0.;
-            mesh.draw_outline(&rect, COLOR_RED, 1.);
-        }
-
-        let mesh = mesh.alloc(&self.render_api);
+        let layout = {
+            let mut txt_ctx = text2::get_ctx().await;
+            txt_ctx.make_layout(&text, text_color, font_size, 0., window_scale, None)
+        };
 
 
-        TextRenderInfo { mesh, texture: atlas.texture }
+        text2::render_layout(&layout, &self.render_api)
     }
     }
 
 
     async fn redraw(self: Arc<Self>) {
     async fn redraw(self: Arc<Self>) {
@@ -274,17 +150,7 @@ impl Text {
         let rect = self.rect.get();
         let rect = self.rect.get();
 
 
         let mut instrs = vec![GfxDrawInstruction::Move(rect.pos())];
         let mut instrs = vec![GfxDrawInstruction::Move(rect.pos())];
-        instrs.append(&mut self.regen_mesh2());
-
-        /*
-        let render_info = self.regen_mesh();
-        let mesh = GfxDrawMesh {
-            vertex_buffer: render_info.mesh.vertex_buffer,
-            index_buffer: render_info.mesh.index_buffer,
-            texture: Some(render_info.texture),
-            num_elements: render_info.mesh.num_elements,
-        };
-        */
+        instrs.append(&mut self.regen_mesh().await);
 
 
         Some(DrawUpdate {
         Some(DrawUpdate {
             key: self.dc_key,
             key: self.dc_key,

+ 1 - 43
bin/app/src/ui/win.rs

@@ -27,7 +27,7 @@ use crate::{
     pubsub::Subscription,
     pubsub::Subscription,
     scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
     scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
     util::unixtime,
     util::unixtime,
-    ExecutorPtr,
+    AndroidSuggestEvent, ExecutorPtr,
 };
 };
 
 
 use super::{get_children_ordered, get_ui_object3, get_ui_object_ptr, OnModify};
 use super::{get_children_ordered, get_ui_object3, get_ui_object_ptr, OnModify};
@@ -161,32 +161,7 @@ impl Window {
             mouse_wheel_task,
             mouse_wheel_task,
             touch_task,
             touch_task,
         ];
         ];
-
         tasks.append(&mut on_modify.tasks);
         tasks.append(&mut on_modify.tasks);
-
-        #[cfg(target_os = "android")]
-        {
-            let (sender, recvr) = async_channel::unbounded();
-            crate::android::set_sender(sender);
-            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 modify_task was stopped!");
-                    };
-
-                    self_.handle_autosuggest(ev).await;
-                }
-            });
-            tasks.push(autosuggest_task);
-        }
-
         self.tasks.set(tasks);
         self.tasks.set(tasks);
 
 
         for child in self.get_children() {
         for child in self.get_children() {
@@ -433,23 +408,6 @@ impl Window {
         }
         }
     }
     }
 
 
-    #[cfg(target_os = "android")]
-    async fn handle_autosuggest(&self, ev: crate::android::AndroidSuggestEvent) {
-        use crate::android::AndroidSuggestEvent::*;
-        for child in self.get_children() {
-            let obj = get_ui_object3(&child);
-            let is_handled = match &ev {
-                Compose { text, cursor_pos, is_commit } => {
-                    obj.handle_compose_text(&text, *is_commit).await
-                }
-                ComposeRegion { start, end } => obj.handle_set_compose_region(*start, *end).await,
-            };
-            if is_handled {
-                return
-            }
-        }
-    }
-
     pub async fn draw(&self) {
     pub async fn draw(&self) {
         let atom = &mut PropertyAtomicGuard::new();
         let atom = &mut PropertyAtomicGuard::new();
         let trace_id = rand::random();
         let trace_id = rand::random();

+ 1 - 0
script/research/codes/reed-solomon.sage

@@ -7,6 +7,7 @@ K = GF(q)
 F.<z> = K[]
 F.<z> = K[]
 
 
 f = 4 + 3*z + 1*z^2 + 5*z^3
 f = 4 + 3*z + 1*z^2 + 5*z^3
+assert len(list(f)) == k
 
 
 # Let βᵢ = K(i - 1)
 # Let βᵢ = K(i - 1)
 
 

Некоторые файлы не были показаны из-за большого количества измененных файлов