mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-09 14:48:08 -05:00
app: completely rework text editing subsystem. initial draft version
This commit is contained in:
32
bin/app/Cargo.lock
generated
32
bin/app/Cargo.lock
generated
@@ -1304,6 +1304,7 @@ dependencies = [
|
||||
"miniquad",
|
||||
"parking_lot 0.12.3",
|
||||
"parley",
|
||||
"peniko",
|
||||
"rand",
|
||||
"regex",
|
||||
"rusqlite",
|
||||
@@ -1311,11 +1312,11 @@ dependencies = [
|
||||
"simplelog",
|
||||
"sled-overlay",
|
||||
"smol",
|
||||
"swash",
|
||||
"swash 0.2.1",
|
||||
"thiserror 2.0.12",
|
||||
"tor-dirmgr 0.26.0",
|
||||
"url",
|
||||
"zeno",
|
||||
"zeno 0.3.2 (git+https://github.com/valadaptive/zeno?branch=tight-bounds)",
|
||||
"zeromq",
|
||||
]
|
||||
|
||||
@@ -2025,7 +2026,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "fontique"
|
||||
version = "0.3.0"
|
||||
source = "git+https://github.com/linebender/parley.git#4bb6859668815e717491ce55e6fd9390e70b0e9a"
|
||||
source = "git+https://github.com/linebender/parley.git#d53a05820799a88b23a9712d9d026378a2699403"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"fontconfig-cache-parser",
|
||||
@@ -3210,7 +3211,7 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
[[package]]
|
||||
name = "miniquad"
|
||||
version = "0.4.8"
|
||||
source = "git+https://github.com/not-fl3/miniquad#613e9959c9cc7fca72c6c96eaf74ecced99165e3"
|
||||
source = "git+https://github.com/narodnik/miniquad#009352f4348f2d9ea9752246d219438d7af8c75c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"ndk-sys",
|
||||
@@ -3628,13 +3629,13 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "parley"
|
||||
version = "0.3.0"
|
||||
source = "git+https://github.com/linebender/parley.git#4bb6859668815e717491ce55e6fd9390e70b0e9a"
|
||||
source = "git+https://github.com/linebender/parley.git#d53a05820799a88b23a9712d9d026378a2699403"
|
||||
dependencies = [
|
||||
"fontique",
|
||||
"hashbrown 0.15.2",
|
||||
"peniko",
|
||||
"skrifa",
|
||||
"swash",
|
||||
"swash 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5007,7 +5008,18 @@ source = "git+https://github.com/valadaptive/swash?branch=tight-bounds#2a6429fb8
|
||||
dependencies = [
|
||||
"skrifa",
|
||||
"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]]
|
||||
@@ -7755,6 +7767,12 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeno"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc0de2315dc13d00e5df3cd6b8d2124a6eaec6a2d4b6a1c5f37b7efad17fcc17"
|
||||
|
||||
[[package]]
|
||||
name = "zeno"
|
||||
version = "0.3.2"
|
||||
|
||||
@@ -9,7 +9,7 @@ homepage = "https://dark.fi"
|
||||
repository = "https://codeberg.org/darkrenaissance/darkfi"
|
||||
|
||||
[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
|
||||
#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" }
|
||||
# Use same zeno specified by swash
|
||||
zeno = { git = "https://github.com/valadaptive/zeno", branch = "tight-bounds" }
|
||||
peniko = "*"
|
||||
|
||||
[features]
|
||||
emulate-android = []
|
||||
|
||||
@@ -45,20 +45,21 @@ ENV NDK_HOME /usr/local/android-ndk-r25
|
||||
|
||||
# Copy contents to container. Should only use this on a clean directory
|
||||
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
|
||||
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
|
||||
RUN cargo install --path /root/cargo-apk
|
||||
|
||||
# Add build-tools to PATH, for apksigner
|
||||
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/
|
||||
|
||||
|
||||
@@ -74,17 +74,17 @@ dev: $(SRC) fonts
|
||||
./darkfi-app
|
||||
|
||||
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) .
|
||||
-adb uninstall darkfi.app
|
||||
adb install darkfi-app.apk
|
||||
-adb uninstall darkfi.darkfi_app
|
||||
adb install -r darkfi-app.apk
|
||||
reset
|
||||
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
|
||||
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:
|
||||
podman run -v $(shell pwd):/root/dw -w /root/dw -t apk rm -fr target/
|
||||
|
||||
@@ -1,26 +1,102 @@
|
||||
//% 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;
|
||||
|
||||
//% END
|
||||
|
||||
//% 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() {
|
||||
return getApplicationContext().getDataDir().getAbsolutePath();
|
||||
}
|
||||
public String getExternalStoragePath() {
|
||||
return getApplicationContext().getExternalFilesDir(null).getAbsolutePath();
|
||||
//return Environment.getExternalStorageDirectory().getAbsolutePath();
|
||||
}
|
||||
|
||||
public int getKeyboardHeight() {
|
||||
@@ -35,16 +111,14 @@ public int getKeyboardHeight() {
|
||||
|
||||
//% 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
|
||||
|
||||
|
||||
@@ -44,8 +44,8 @@ import android.view.inputmethod.InputMethodManager;
|
||||
// It then adapts android's IME to chrome's RenderWidgetHostView using the
|
||||
// native ImeAdapterAndroid via the outer class ImeAdapter.
|
||||
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 ImeAdapter mImeAdapter;
|
||||
@@ -54,12 +54,15 @@ public class CustomInputConnection extends BaseInputConnection {
|
||||
private int numBatchEdits;
|
||||
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) {
|
||||
public CustomInputConnection(View view, EditorInfo outAttrs) {
|
||||
public CustomInputConnection(int id, View view, EditorInfo outAttrs) {
|
||||
super(view, true);
|
||||
this.id = id;
|
||||
log("CustomInputConnection()");
|
||||
mInternalView = view;
|
||||
//mImeAdapter = imeAdapter;
|
||||
@@ -117,7 +120,7 @@ public class CustomInputConnection extends BaseInputConnection {
|
||||
|
||||
private void log(String fstr, Object... args) {
|
||||
if (!DEBUG) return;
|
||||
String text = "(" + ID + "): " + String.format(fstr, args);
|
||||
String text = "(" + id + "): " + String.format(fstr, args);
|
||||
Log.d("darkfi", text);
|
||||
}
|
||||
|
||||
@@ -138,11 +141,12 @@ public class CustomInputConnection extends BaseInputConnection {
|
||||
*/
|
||||
public void setEditableText(String text, int selectionStart, int selectionEnd,
|
||||
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);
|
||||
|
||||
if (mEditable == null) {
|
||||
log("setEditableText creating new editable");
|
||||
mEditable = Editable.Factory.getInstance().newEditable("");
|
||||
}
|
||||
|
||||
@@ -176,12 +180,14 @@ public class CustomInputConnection extends BaseInputConnection {
|
||||
}
|
||||
|
||||
if (!textUnchanged) {
|
||||
log("replace mEditable with: %s", text);
|
||||
mEditable.replace(0, mEditable.length(), text);
|
||||
}
|
||||
Selection.setSelection(mEditable, selectionStart, selectionEnd);
|
||||
super.setComposingRegion(compositionStart, compositionEnd);
|
||||
|
||||
if (textUnchanged || prevText.equals("")) {
|
||||
log("setEditableText updating selection");
|
||||
// updateSelection should be called when a manual selection change occurs.
|
||||
// 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.
|
||||
@@ -193,6 +199,7 @@ public class CustomInputConnection extends BaseInputConnection {
|
||||
@Override
|
||||
public Editable getEditable() {
|
||||
if (mEditable == null) {
|
||||
log("getEditable() [create new]");
|
||||
mEditable = Editable.Factory.getInstance().newEditable("");
|
||||
Selection.setSelection(mEditable, 0);
|
||||
}
|
||||
@@ -205,7 +212,7 @@ public class CustomInputConnection extends BaseInputConnection {
|
||||
log("setComposingText(%s, %d)", text, newCursorPosition);
|
||||
super.setComposingText(text, newCursorPosition);
|
||||
shouldUpdateImeSelection = true;
|
||||
onCompose(text.toString(), newCursorPosition, false);
|
||||
onCompose(id, text.toString(), newCursorPosition, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -214,7 +221,7 @@ public class CustomInputConnection extends BaseInputConnection {
|
||||
log("commitText(%s, %d)", text, newCursorPosition);
|
||||
super.commitText(text, newCursorPosition);
|
||||
shouldUpdateImeSelection = true;
|
||||
onCompose(text.toString(), newCursorPosition, text.length() > 0);
|
||||
onCompose(id, text.toString(), newCursorPosition, text.length() > 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -258,6 +265,27 @@ public class CustomInputConnection extends BaseInputConnection {
|
||||
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
|
||||
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
|
||||
log("getExtractedText(...)");
|
||||
@@ -282,6 +310,7 @@ public class CustomInputConnection extends BaseInputConnection {
|
||||
}
|
||||
shouldUpdateImeSelection = true;
|
||||
//return mImeAdapter.deleteSurroundingText(leftLength, rightLength);
|
||||
onDeleteSurroundingText(id, leftLength, rightLength);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -330,7 +359,7 @@ public class CustomInputConnection extends BaseInputConnection {
|
||||
return true;
|
||||
}
|
||||
super.finishComposingText();
|
||||
onCompose("", 0, true);
|
||||
onFinishCompose(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -360,7 +389,7 @@ public class CustomInputConnection extends BaseInputConnection {
|
||||
int a = Math.min(start, end);
|
||||
int b = Math.max(start, end);
|
||||
super.setComposingRegion(a, b);
|
||||
onSetComposeRegion(a, b);
|
||||
onSetComposeRegion(id, a, b);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -369,19 +398,28 @@ public class CustomInputConnection extends BaseInputConnection {
|
||||
}
|
||||
|
||||
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() {
|
||||
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
|
||||
@@ -398,6 +436,7 @@ public class CustomInputConnection extends BaseInputConnection {
|
||||
updateImeSelection();
|
||||
shouldUpdateImeSelection = false;
|
||||
}
|
||||
log("endBatchEdit DONE");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -419,7 +458,15 @@ public class CustomInputConnection extends BaseInputConnection {
|
||||
}
|
||||
|
||||
// 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
|
||||
for (Object span : spans) {
|
||||
@@ -453,5 +500,24 @@ public class CustomInputConnection extends BaseInputConnection {
|
||||
|
||||
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
bin/app/java/autosuggest/InvisibleInputView.java
Normal file
90
bin/app/java/autosuggest/InvisibleInputView.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
main_activity_inject = "java/MainActivity.java"
|
||||
java_files = [
|
||||
"java/autosuggest/CustomInputConnection.java",
|
||||
"java/autosuggest/InvisibleInputView.java",
|
||||
#"java/autosuggest/InvisibleInputManager.java",
|
||||
]
|
||||
|
||||
|
||||
@@ -22,107 +22,277 @@ use std::{
|
||||
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 {
|
||||
sender: Option<async_channel::Sender<AndroidSuggestEvent>>,
|
||||
next_id: usize,
|
||||
}
|
||||
|
||||
unsafe impl Send for GlobalData {}
|
||||
unsafe impl Sync for GlobalData {}
|
||||
|
||||
static GLOBALS: LazyLock<SyncMutex<GlobalData>> =
|
||||
LazyLock::new(|| SyncMutex::new(GlobalData { sender: None }));
|
||||
LazyLock::new(|| SyncMutex::new(GlobalData { sender: None, next_id: 0 }));
|
||||
|
||||
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_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);
|
||||
}
|
||||
}
|
||||
|
||||
#[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]
|
||||
pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_onCompose(
|
||||
env: *mut ndk_sys::JNIEnv,
|
||||
_: ndk_sys::jobject,
|
||||
id: ndk_sys::jint,
|
||||
text: ndk_sys::jobject,
|
||||
cursor_pos: ndk_sys::jint,
|
||||
is_commit: ndk_sys::jboolean,
|
||||
) {
|
||||
assert!(id >= 0);
|
||||
let id = id as usize;
|
||||
let text = ndk_utils::get_utf_str!(env, text);
|
||||
if let Some(sender) = &GLOBALS.lock().unwrap().sender {
|
||||
let _ = sender.try_send(AndroidSuggestEvent::Compose {
|
||||
text: text.to_string(),
|
||||
cursor_pos,
|
||||
is_commit: is_commit != 0,
|
||||
is_commit: is_commit == 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_onSetComposeRegion(
|
||||
env: *mut ndk_sys::JNIEnv,
|
||||
_: ndk_sys::jobject,
|
||||
id: ndk_sys::jint,
|
||||
start: 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
|
||||
}
|
||||
|
||||
let start = begin as usize;
|
||||
let end = end as usize;
|
||||
|
||||
assert!(id >= 0);
|
||||
let id = id as usize;
|
||||
if let Some(sender) = &GLOBALS.lock().unwrap().sender {
|
||||
let _ = sender.try_send(AndroidSuggestEvent::ComposeRegion { start, end });
|
||||
let _ = sender.try_send(AndroidSuggestEvent::ComposeRegion {
|
||||
start: start as usize,
|
||||
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 {
|
||||
let _ = sender.try_send(AndroidSuggestEvent::FinishCompose);
|
||||
}
|
||||
}
|
||||
#[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 set_sender(sender: async_channel::Sender<AndroidSuggestEvent>) {
|
||||
GLOBALS.lock().unwrap().sender = Some(sender);
|
||||
}
|
||||
|
||||
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 {
|
||||
let env = android::attach_jni_env();
|
||||
ndk_utils::call_void_method!(env, android::ACTIVITY, "createComposer", "(I)V", composer_id);
|
||||
}
|
||||
composer_id
|
||||
}
|
||||
|
||||
ndk_utils::call_void_method!(env, android::ACTIVITY, "cancelComposition", "()V");
|
||||
pub fn focus(id: usize) -> Option<()> {
|
||||
let is_success = unsafe {
|
||||
let env = android::attach_jni_env();
|
||||
|
||||
ndk_utils::call_bool_method!(env, android::ACTIVITY, "focus", "(I)Z", id as i32)
|
||||
};
|
||||
if is_success == 0u8 {
|
||||
None
|
||||
} else {
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
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 new_string_utf = (**env).NewStringUTF.unwrap();
|
||||
let jtext = new_string_utf(env, ctext.as_ptr());
|
||||
|
||||
ndk_utils::call_bool_method!(
|
||||
env,
|
||||
android::ACTIVITY,
|
||||
"setText",
|
||||
"(ILjava/lang/String;)Z",
|
||||
id as i32,
|
||||
jtext
|
||||
)
|
||||
};
|
||||
if is_success == 0u8 {
|
||||
None
|
||||
} else {
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
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 input_connect = ndk_utils::call_object_method!(
|
||||
env,
|
||||
android::ACTIVITY,
|
||||
"getInputConnect",
|
||||
"(I)Lautosuggest/CustomInputConnection;",
|
||||
id as i32
|
||||
);
|
||||
if input_connect.is_null() {
|
||||
return None
|
||||
}
|
||||
|
||||
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,
|
||||
android::ACTIVITY,
|
||||
"getInputConnect",
|
||||
"(I)Lautosuggest/CustomInputConnection;",
|
||||
id as i32
|
||||
);
|
||||
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 {
|
||||
unsafe {
|
||||
let env = android::attach_jni_env();
|
||||
|
||||
ndk_utils::call_int_method!(env, android::ACTIVITY, "getKeyboardHeight", "()I") as usize
|
||||
}
|
||||
call_mainactivity_int_method!("getKeyboardHeight", "()I") as usize
|
||||
}
|
||||
|
||||
pub fn get_appdata_path() -> PathBuf {
|
||||
let path = unsafe {
|
||||
let env = android::attach_jni_env();
|
||||
|
||||
let text = ndk_utils::call_object_method!(
|
||||
env,
|
||||
android::ACTIVITY,
|
||||
"getAppDataPath",
|
||||
"()Ljava/lang/String;"
|
||||
);
|
||||
ndk_utils::get_utf_str!(env, text).to_string()
|
||||
};
|
||||
path.into()
|
||||
call_mainactivity_str_method!("getAppDataPath").into()
|
||||
}
|
||||
pub fn get_external_storage_path() -> PathBuf {
|
||||
let path = unsafe {
|
||||
let env = android::attach_jni_env();
|
||||
|
||||
let text = ndk_utils::call_object_method!(
|
||||
env,
|
||||
android::ACTIVITY,
|
||||
"getExternalStoragePath",
|
||||
"()Ljava/lang/String;"
|
||||
);
|
||||
ndk_utils::get_utf_str!(env, text).to_string()
|
||||
};
|
||||
path.into()
|
||||
call_mainactivity_str_method!("getExternalStoragePath").into()
|
||||
}
|
||||
|
||||
@@ -314,17 +314,14 @@ pub fn create_chatedit(name: &str) -> SceneNode {
|
||||
prop.set_ui_text("Is Focused", "A focused EditBox receives input");
|
||||
node.add_property(prop).unwrap();
|
||||
|
||||
let mut prop = Property::new("min_height", PropertyType::Float32, PropertySubType::Pixel);
|
||||
prop.set_ui_text("Min Height", "Minimum 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_array_len(2);
|
||||
node.add_property(prop).unwrap();
|
||||
|
||||
let mut prop = Property::new("max_height", PropertyType::Float32, PropertySubType::Pixel);
|
||||
prop.set_ui_text("Max Height", "Maximum height");
|
||||
prop.set_range_f32(0., f32::MAX);
|
||||
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();
|
||||
|
||||
let mut prop = Property::new("rect", PropertyType::Float32, PropertySubType::Pixel);
|
||||
@@ -332,6 +329,7 @@ pub fn create_chatedit(name: &str) -> SceneNode {
|
||||
prop.allow_exprs();
|
||||
node.add_property(prop).unwrap();
|
||||
|
||||
// DEPRECATED ------------
|
||||
let prop = Property::new("baseline", PropertyType::Float32, PropertySubType::Pixel);
|
||||
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);
|
||||
node.add_property(prop).unwrap();
|
||||
//------------------------
|
||||
|
||||
let mut prop = Property::new("lineheight", PropertyType::Float32, PropertySubType::Pixel);
|
||||
prop.set_ui_text("Line Height", "Line height/lead (em)");
|
||||
prop.set_defaults_f32(vec![1.2]).unwrap();
|
||||
prop.set_range_f32(0., f32::MAX);
|
||||
node.add_property(prop).unwrap();
|
||||
|
||||
let mut prop = Property::new("scroll", PropertyType::Float32, PropertySubType::Pixel);
|
||||
prop.set_range_f32(0., f32::MAX);
|
||||
|
||||
@@ -479,16 +479,19 @@ pub async fn make(app: &App, window: SceneNodePtr) {
|
||||
})
|
||||
.await;
|
||||
layer_node.clone().link(node);
|
||||
*/
|
||||
|
||||
// Text edit
|
||||
let node = create_chatedit("editz");
|
||||
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_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();
|
||||
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_expr(atom, Role::App, 2, expr::load_var("parent_w")).unwrap();
|
||||
prop.clone().set_f32(atom, Role::App, 3, 50.).unwrap();
|
||||
@@ -536,5 +539,4 @@ pub async fn make(app: &App, window: SceneNodePtr) {
|
||||
})
|
||||
.await;
|
||||
layer_node.clone().link(node);
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ pub fn setup_logging() {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
use android::AndroidLoggerWrapper;
|
||||
let android_logger = AndroidLoggerWrapper::new(LevelFilter::Debug, cfg);
|
||||
let android_logger = AndroidLoggerWrapper::new(LevelFilter::Trace, cfg);
|
||||
loggers.push(android_logger);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,16 @@ extern crate log;
|
||||
#[allow(unused_imports)]
|
||||
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")]
|
||||
mod android;
|
||||
mod app;
|
||||
@@ -68,6 +78,7 @@ mod scene;
|
||||
mod shape;
|
||||
use scene::SceneNode as SceneNode3;
|
||||
mod text;
|
||||
mod text2;
|
||||
mod ui;
|
||||
mod util;
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ use crate::gfx::Rectangle;
|
||||
|
||||
mod atlas;
|
||||
pub use atlas::{make_texture_atlas, Atlas, RenderedAtlas};
|
||||
pub mod atlas2;
|
||||
mod ft;
|
||||
use ft::{render_glyph, FreetypeFace, Sprite, SpritePtr};
|
||||
mod shape;
|
||||
|
||||
285
bin/app/src/text2/atlas.rs
Normal file
285
bin/app/src/text2/atlas.rs
Normal file
@@ -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
bin/app/src/text2/editor/android.rs
Normal file
152
bin/app/src/text2/editor/android.rs
Normal file
@@ -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
bin/app/src/text2/editor/mod.rs
Normal file
27
bin/app/src/text2/editor/mod.rs
Normal file
@@ -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
bin/app/src/text2/editor/parley.rs
Normal file
118
bin/app/src/text2/editor/parley.rs
Normal file
@@ -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
bin/app/src/text2/mod.rs
Normal file
92
bin/app/src/text2/mod.rs
Normal file
@@ -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
bin/app/src/text2/render.rs
Normal file
143
bin/app/src/text2/render.rs
Normal file
@@ -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)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -231,8 +231,8 @@ impl Editable {
|
||||
|
||||
/// Reset any composition in progress
|
||||
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:?}]");
|
||||
let final_text = self.composer.clear();
|
||||
|
||||
@@ -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})");
|
||||
|
||||
if !self.is_active.get() {
|
||||
@@ -1593,6 +1599,7 @@ impl UIObject for EditBox {
|
||||
|
||||
true
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/// Filter these char events from being handled since we handle them
|
||||
|
||||
@@ -31,7 +31,7 @@ use crate::{
|
||||
},
|
||||
scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
|
||||
util::unixtime,
|
||||
ExecutorPtr,
|
||||
AndroidSuggestEvent, ExecutorPtr,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -305,28 +305,4 @@ impl UIObject for Layer {
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ use crate::{
|
||||
gfx::{GfxBufferId, GfxDrawCall, GfxDrawMesh, GfxTextureId, Point, Rectangle},
|
||||
prop::{ModifyAction, PropertyAtomicGuard, PropertyPtr, Role},
|
||||
scene::{Pimpl, SceneNode as SceneNode3, SceneNodeId, SceneNodePtr, SceneNodeWeak},
|
||||
ExecutorPtr,
|
||||
AndroidSuggestEvent, ExecutorPtr,
|
||||
};
|
||||
|
||||
mod button;
|
||||
@@ -104,14 +104,6 @@ pub trait UIObject: Sync {
|
||||
async fn handle_touch(&self, phase: TouchPhase, id: u64, touch_pos: Point) -> bool {
|
||||
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 {
|
||||
|
||||
@@ -32,6 +32,7 @@ use crate::{
|
||||
},
|
||||
scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
|
||||
text::{self, GlyphPositionIter, TextShaper, TextShaperPtr},
|
||||
text2,
|
||||
util::unixtime,
|
||||
ExecutorPtr,
|
||||
};
|
||||
@@ -116,143 +117,18 @@ impl Text {
|
||||
Pimpl::Text(self_)
|
||||
}
|
||||
|
||||
fn regen_mesh2(&self) -> Vec<GfxDrawInstruction> {
|
||||
async fn regen_mesh(&self) -> Vec<GfxDrawInstruction> {
|
||||
let text = self.text.get();
|
||||
let font_size = self.font_size.get();
|
||||
let text_color = self.text_color.get();
|
||||
let window_scale = self.window_scale.get();
|
||||
|
||||
let mut layout_cx = parley::LayoutContext::new();
|
||||
let mut font_cx = parley::FontContext::new();
|
||||
let layout = {
|
||||
let mut txt_ctx = text2::get_ctx().await;
|
||||
txt_ctx.make_layout(&text, text_color, font_size, 0., window_scale, None)
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
TextRenderInfo { mesh, texture: atlas.texture }
|
||||
text2::render_layout(&layout, &self.render_api)
|
||||
}
|
||||
|
||||
async fn redraw(self: Arc<Self>) {
|
||||
@@ -274,17 +150,7 @@ impl Text {
|
||||
let rect = self.rect.get();
|
||||
|
||||
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 {
|
||||
key: self.dc_key,
|
||||
|
||||
@@ -27,7 +27,7 @@ use crate::{
|
||||
pubsub::Subscription,
|
||||
scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
|
||||
util::unixtime,
|
||||
ExecutorPtr,
|
||||
AndroidSuggestEvent, ExecutorPtr,
|
||||
};
|
||||
|
||||
use super::{get_children_ordered, get_ui_object3, get_ui_object_ptr, OnModify};
|
||||
@@ -161,32 +161,7 @@ impl Window {
|
||||
mouse_wheel_task,
|
||||
touch_task,
|
||||
];
|
||||
|
||||
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);
|
||||
|
||||
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) {
|
||||
let atom = &mut PropertyAtomicGuard::new();
|
||||
let trace_id = rand::random();
|
||||
|
||||
@@ -7,6 +7,7 @@ K = GF(q)
|
||||
F.<z> = K[]
|
||||
|
||||
f = 4 + 3*z + 1*z^2 + 5*z^3
|
||||
assert len(list(f)) == k
|
||||
|
||||
# Let βᵢ = K(i - 1)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user