diff --git a/Cargo.lock b/Cargo.lock
index f9b0337..56f4a05 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -62,6 +62,7 @@ dependencies = [
"send_wrapper",
"smallvec",
"ui-events",
+ "ui-text-input",
]
[[package]]
@@ -85,19 +86,6 @@ dependencies = [
"vello",
]
-[[package]]
-name = "android-view-masonry-demo"
-version = "0.1.0"
-dependencies = [
- "android-view",
- "masonry",
- "masonry_android",
- "profiling",
- "tracing",
- "tracing-subscriber",
- "tracing_android_trace",
-]
-
[[package]]
name = "android_log-sys"
version = "0.3.2"
@@ -139,17 +127,6 @@ version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
-[[package]]
-name = "anymap3"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "170433209e817da6aae2c51aa0dd443009a613425dd041ebfb2492d1c4c11a25"
-
-[[package]]
-name = "anymore"
-version = "0.1.0"
-source = "git+https://github.com/linebender/anymore?rev=3537bdc3aa03c92e380f106d18b7957b934c9eb5#3537bdc3aa03c92e380f106d18b7957b934c9eb5"
-
[[package]]
name = "arrayvec"
version = "0.7.6"
@@ -319,21 +296,6 @@ dependencies = [
"cfg-if",
]
-[[package]]
-name = "cursor-icon"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
-
-[[package]]
-name = "deranged"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
-dependencies = [
- "powerfmt",
-]
-
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -632,12 +594,6 @@ dependencies = [
"hashbrown",
]
-[[package]]
-name = "itoa"
-version = "1.0.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
-
[[package]]
name = "jni"
version = "0.21.1"
@@ -707,6 +663,18 @@ dependencies = [
"smallvec",
]
+[[package]]
+name = "kurbo"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2"
+dependencies = [
+ "arrayvec",
+ "euclid",
+ "polycool",
+ "smallvec",
+]
+
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -766,63 +734,6 @@ dependencies = [
"libc",
]
-[[package]]
-name = "masonry"
-version = "0.3.0"
-source = "git+https://github.com/linebender/xilem#e14ba3a5f9461b403cb30d95826187fba7f6924b"
-dependencies = [
- "accesskit",
- "cursor-icon",
- "dpi",
- "masonry_core",
- "parley",
- "tracing",
- "ui-events",
- "vello",
-]
-
-[[package]]
-name = "masonry_android"
-version = "0.1.0"
-dependencies = [
- "accesskit_android",
- "android-view",
- "masonry_core",
- "pollster",
- "tracing",
-]
-
-[[package]]
-name = "masonry_core"
-version = "0.3.0"
-source = "git+https://github.com/linebender/xilem#e14ba3a5f9461b403cb30d95826187fba7f6924b"
-dependencies = [
- "accesskit",
- "anymap3",
- "anymore",
- "cursor-icon",
- "dpi",
- "parley",
- "smallvec",
- "time",
- "tracing",
- "tracing-subscriber",
- "tracing_android_trace",
- "tree_arena",
- "ui-events",
- "vello",
- "web-time",
-]
-
-[[package]]
-name = "matchers"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
-dependencies = [
- "regex-automata 0.1.10",
-]
-
[[package]]
name = "memchr"
version = "2.7.4"
@@ -928,12 +839,6 @@ dependencies = [
"winapi",
]
-[[package]]
-name = "num-conv"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
-
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -1087,7 +992,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f9529efd019889b2a205193c14ffb6e2839b54ed9d2720674f10f4b04d87ac9"
dependencies = [
"color",
- "kurbo",
+ "kurbo 0.11.2",
"smallvec",
]
@@ -1123,10 +1028,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
[[package]]
-name = "powerfmt"
-version = "0.2.0"
+name = "polycool"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6"
+dependencies = [
+ "arrayvec",
+]
[[package]]
name = "presser"
@@ -1220,17 +1128,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
- "regex-automata 0.4.9",
- "regex-syntax 0.8.5",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
-dependencies = [
- "regex-syntax 0.6.29",
+ "regex-automata",
+ "regex-syntax",
]
[[package]]
@@ -1241,15 +1140,9 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
- "regex-syntax 0.8.5",
+ "regex-syntax",
]
-[[package]]
-name = "regex-syntax"
-version = "0.6.29"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
-
[[package]]
name = "regex-syntax"
version = "0.8.5"
@@ -1357,9 +1250,9 @@ dependencies = [
[[package]]
name = "smallvec"
-version = "1.15.0"
+version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "spirv"
@@ -1485,37 +1378,6 @@ dependencies = [
"once_cell",
]
-[[package]]
-name = "time"
-version = "0.3.41"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
-dependencies = [
- "deranged",
- "itoa",
- "num-conv",
- "powerfmt",
- "serde",
- "time-core",
- "time-macros",
-]
-
-[[package]]
-name = "time-core"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
-
-[[package]]
-name = "time-macros"
-version = "0.2.22"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
-dependencies = [
- "num-conv",
- "time-core",
-]
-
[[package]]
name = "tinystr"
version = "0.7.6"
@@ -1592,15 +1454,10 @@ version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
- "matchers",
"nu-ansi-term",
- "once_cell",
- "regex",
"sharded-slab",
"smallvec",
"thread_local",
- "time",
- "tracing",
"tracing-core",
"tracing-log",
]
@@ -1620,21 +1477,22 @@ dependencies = [
]
[[package]]
-name = "tree_arena"
-version = "0.1.0"
-source = "git+https://github.com/linebender/xilem#e14ba3a5f9461b403cb30d95826187fba7f6924b"
+name = "ui-events"
+version = "0.3.0"
+source = "git+https://github.com/waywardmonkeys/ui-events.git?rev=c46ff06ea4c18636aed62f20417e797505fcb211#c46ff06ea4c18636aed62f20417e797505fcb211"
dependencies = [
- "hashbrown",
+ "dpi",
+ "keyboard-types",
+ "kurbo 0.13.1",
]
[[package]]
-name = "ui-events"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4c2cc34489c685d4e7a1a1f97b7b4416c5aa789892114ae77df6cd2a60f0ec4"
+name = "ui-text-input"
+version = "0.3.0"
+source = "git+https://github.com/waywardmonkeys/ui-events.git?rev=c46ff06ea4c18636aed62f20417e797505fcb211#c46ff06ea4c18636aed62f20417e797505fcb211"
dependencies = [
"dpi",
- "keyboard-types",
+ "ui-events",
]
[[package]]
@@ -1802,16 +1660,6 @@ dependencies = [
"wasm-bindgen",
]
-[[package]]
-name = "web-time"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
-dependencies = [
- "js-sys",
- "wasm-bindgen",
-]
-
[[package]]
name = "wgpu"
version = "24.0.3"
diff --git a/Cargo.toml b/Cargo.toml
index f1ef3a0..8de94e4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,10 +2,12 @@
members = [
".",
"demo",
- "masonry",
- "masonry-demo",
]
+[workspace.dependencies]
+ui-events = { git = "https://github.com/waywardmonkeys/ui-events.git", rev = "c46ff06ea4c18636aed62f20417e797505fcb211" }
+ui-text-input = { git = "https://github.com/waywardmonkeys/ui-events.git", rev = "c46ff06ea4c18636aed62f20417e797505fcb211" }
+
[package]
name = "android-view"
version = "0.1.0"
@@ -18,7 +20,8 @@ ndk = "0.9.0"
num_enum = "0.7.3"
send_wrapper = "0.6.0"
smallvec = "1.15.0"
-ui-events = "0.1.0"
+ui-events.workspace = true
+ui-text-input.workspace = true
[profile.dev]
panic = "abort"
diff --git a/README.md b/README.md
index e1decb7..9affa76 100644
--- a/README.md
+++ b/README.md
@@ -30,17 +30,6 @@ adb shell am start -n org.linebender.android.viewdemo/.DemoActivity
adb shell run-as org.linebender.android.viewdemo logcat -v color
```
-### Masonry demo
-
-```bash
-cargo ndk -t arm64-v8a -o masonry-app/src/main/jniLibs/ build -p android-view-masonry-demo
-./gradlew build
-./gradlew installDebug
-adb shell am start -n org.linebender.android.masonrydemo/.DemoActivity
-# To view logs:
-adb shell run-as org.linebender.android.masonrydemo logcat -v color
-```
-
## Open questions
* Do we need to be able to handle the view being reattached to a window after it has been detached? If not, then `onDetachedFromWindow` is the logical place to sever the connection between Java and native.
diff --git a/demo/Cargo.toml b/demo/Cargo.toml
index fb34ce5..c3bbe22 100644
--- a/demo/Cargo.toml
+++ b/demo/Cargo.toml
@@ -17,7 +17,7 @@ log = "0.4.26"
parley = { git = "https://github.com/linebender/parley", rev = "587b7634ae8601c10de7f0361bfd56085a5b7b4e", features = ["accesskit"] }
peniko = { version = "0.4.0", default-features = false }
pollster = "0.4.0"
-ui-events = "0.1.0"
+ui-events.workspace = true
vello = "0.5.0"
# Send tracing events to Android GPU inspector, for profiling
diff --git a/demo/src/lib.rs b/demo/src/lib.rs
index d34671b..e1af7ce 100644
--- a/demo/src/lib.rs
+++ b/demo/src/lib.rs
@@ -371,7 +371,7 @@ impl ViewPeer for DemoViewPeer {
return false;
};
- if matches!(ev, PointerEvent::Up { .. }) {
+ if matches!(ev, PointerEvent::Up(..)) {
ctx.push_static_deferred_callback(show_soft_input);
}
diff --git a/demo/src/text.rs b/demo/src/text.rs
index 1c68a99..4ee157f 100644
--- a/demo/src/text.rs
+++ b/demo/src/text.rs
@@ -7,12 +7,12 @@ use core::default::Default;
pub use parley::editor::Generation;
use parley::{
FontContext, GenericFamily, LayoutContext, PlainEditor, PlainEditorDriver, StyleProperty,
- editor::SplitString, layout::PositionedLayoutItem,
+ layout::PositionedLayoutItem,
};
use std::time::{Duration, Instant};
use ui_events::{
keyboard::{Code, Key, KeyState, KeyboardEvent, NamedKey},
- pointer::{PointerButton, PointerEvent, PointerState, PointerUpdate},
+ pointer::{PointerButton, PointerButtonEvent, PointerEvent, PointerState, PointerUpdate},
};
use vello::{
Scene,
@@ -66,10 +66,6 @@ impl Editor {
&mut self.editor
}
- pub fn text(&self) -> SplitString<'_> {
- self.editor.text()
- }
-
pub fn utf8_to_utf16_index(&self, utf8_index: usize) -> usize {
let mut utf16_len_so_far = 0usize;
let mut utf8_len_so_far = 0usize;
@@ -110,16 +106,12 @@ impl Editor {
}
pub fn usv_to_utf8_index(&self, usv_index: usize) -> usize {
- let mut usv_len_so_far = 0usize;
- let mut utf8_len_so_far = 0usize;
- for c in self.editor.raw_text().chars() {
- if usv_len_so_far >= usv_index {
- break;
- }
- usv_len_so_far += 1;
- utf8_len_so_far += c.len_utf8();
- }
- utf8_len_so_far
+ self.editor
+ .raw_text()
+ .chars()
+ .take(usv_index)
+ .map(char::len_utf8)
+ .sum()
}
pub fn cursor_reset(&mut self) {
@@ -148,7 +140,7 @@ impl Editor {
pub fn cursor_blink(&mut self) {
self.cursor_visible = self.start_time.is_some_and(|start_time| {
let elapsed = Instant::now().duration_since(start_time);
- (elapsed.as_millis() / self.blink_period.as_millis()) % 2 == 0
+ (elapsed.as_millis() / self.blink_period.as_millis()).is_multiple_of(2)
});
}
@@ -310,7 +302,7 @@ impl Editor {
pub fn handle_pointer_event(&mut self, ev: PointerEvent) -> bool {
let mut drv = self.editor.driver(&mut self.font_cx, &mut self.layout_cx);
match ev {
- PointerEvent::Down {
+ PointerEvent::Down(PointerButtonEvent {
button: None | Some(PointerButton::Primary),
state:
PointerState {
@@ -320,7 +312,7 @@ impl Editor {
..
},
..
- } => match count {
+ }) => match count {
2 => drv.select_word_at_point(position.x as f32 - INSET, position.y as f32 - INSET),
3 => drv.select_line_at_point(position.x as f32 - INSET, position.y as f32 - INSET),
1 if modifiers.shift() => drv.extend_selection_to_point(
@@ -346,10 +338,10 @@ impl Editor {
}
pub fn handle_accesskit_action_request(&mut self, req: &accesskit::ActionRequest) {
- if req.action == accesskit::Action::SetTextSelection {
- if let Some(accesskit::ActionData::SetTextSelection(selection)) = &req.data {
- self.driver().select_from_accesskit(selection);
- }
+ if req.action == accesskit::Action::SetTextSelection
+ && let Some(accesskit::ActionData::SetTextSelection(selection)) = &req.data
+ {
+ self.driver().select_from_accesskit(selection);
}
}
@@ -372,16 +364,16 @@ impl Editor {
&rect,
);
});
- if self.cursor_visible {
- if let Some(cursor) = self.editor.cursor_geometry(5.0) {
- scene.fill(
- Fill::NonZero,
- transform,
- palette::css::CADET_BLUE,
- None,
- &cursor,
- );
- }
+ if self.cursor_visible
+ && let Some(cursor) = self.editor.cursor_geometry(5.0)
+ {
+ scene.fill(
+ Fill::NonZero,
+ transform,
+ palette::css::CADET_BLUE,
+ None,
+ &cursor,
+ );
}
let layout = self.editor.layout(&mut self.font_cx, &mut self.layout_cx);
for line in layout.lines() {
diff --git a/masonry-app/build.gradle b/masonry-app/build.gradle
deleted file mode 100644
index 13c7deb..0000000
--- a/masonry-app/build.gradle
+++ /dev/null
@@ -1,45 +0,0 @@
-plugins {
- id 'com.android.application'
-}
-
-group = "org.linebender.android.rustview"
-
-android {
- ndkVersion "25.2.9519653"
- compileSdk 31
-
- defaultConfig {
- applicationId "org.linebender.android.masonrydemo"
- minSdk 28
- targetSdk 33
- versionCode 1
- versionName "1.0"
-
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- }
-
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- }
- debug {
- minifyEnabled false
- //packagingOptions {
- // doNotStrip '**/*.so'
- //}
- // debuggable true
- }
- }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
- namespace "org.linebender.android.masonrydemo"
-}
-
-dependencies {
- implementation 'androidx.appcompat:appcompat:1.2.0'
- implementation 'androidx.core:core:1.5.0'
- implementation project(":library")
-}
diff --git a/masonry-app/proguard-rules.pro b/masonry-app/proguard-rules.pro
deleted file mode 100644
index 481bb43..0000000
--- a/masonry-app/proguard-rules.pro
+++ /dev/null
@@ -1,21 +0,0 @@
-# Add project specific ProGuard rules here.
-# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
-
-# Uncomment this to preserve the line number information for
-# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
-
-# If you keep the line number information, uncomment this to
-# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/masonry-app/src/main/AndroidManifest.xml b/masonry-app/src/main/AndroidManifest.xml
deleted file mode 100644
index da5126b..0000000
--- a/masonry-app/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/masonry-app/src/main/java/org/linebender/android/masonrydemo/DemoActivity.java b/masonry-app/src/main/java/org/linebender/android/masonrydemo/DemoActivity.java
deleted file mode 100644
index 88ee4e5..0000000
--- a/masonry-app/src/main/java/org/linebender/android/masonrydemo/DemoActivity.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.linebender.android.masonrydemo;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.view.View;
-import android.widget.FrameLayout;
-
-public final class DemoActivity extends Activity {
- static {
- System.loadLibrary("main");
- }
-
- @Override
- public void onCreate(Bundle state) {
- super.onCreate(state);
- View view = new DemoView(this);
- view.setLayoutParams(
- new FrameLayout.LayoutParams(
- FrameLayout.LayoutParams.MATCH_PARENT,
- FrameLayout.LayoutParams.MATCH_PARENT));
- view.setFocusable(true);
- view.setFocusableInTouchMode(true);
- FrameLayout layout = new FrameLayout(this);
- layout.addView(view);
- setContentView(layout);
- view.requestFocus();
- }
-}
diff --git a/masonry-app/src/main/java/org/linebender/android/masonrydemo/DemoView.java b/masonry-app/src/main/java/org/linebender/android/masonrydemo/DemoView.java
deleted file mode 100644
index ae031b4..0000000
--- a/masonry-app/src/main/java/org/linebender/android/masonrydemo/DemoView.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.linebender.android.masonrydemo;
-
-import android.content.Context;
-
-import org.linebender.android.rustview.RustView;
-
-public final class DemoView extends RustView {
- @Override
- protected native long newViewPeer(Context context);
-
- public DemoView(Context context) {
- super(context);
- }
-}
diff --git a/masonry-app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/masonry-app/src/main/res/drawable-v24/ic_launcher_foreground.xml
deleted file mode 100644
index 2b068d1..0000000
--- a/masonry-app/src/main/res/drawable-v24/ic_launcher_foreground.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/masonry-app/src/main/res/drawable/ic_launcher_background.xml b/masonry-app/src/main/res/drawable/ic_launcher_background.xml
deleted file mode 100644
index 07d5da9..0000000
--- a/masonry-app/src/main/res/drawable/ic_launcher_background.xml
+++ /dev/null
@@ -1,170 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/masonry-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/masonry-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
deleted file mode 100644
index eca70cf..0000000
--- a/masonry-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/masonry-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/masonry-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
deleted file mode 100644
index eca70cf..0000000
--- a/masonry-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/masonry-app/src/main/res/mipmap-hdpi/ic_launcher.webp b/masonry-app/src/main/res/mipmap-hdpi/ic_launcher.webp
deleted file mode 100644
index c209e78..0000000
Binary files a/masonry-app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ
diff --git a/masonry-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/masonry-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
deleted file mode 100644
index b2dfe3d..0000000
Binary files a/masonry-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/masonry-app/src/main/res/mipmap-mdpi/ic_launcher.webp b/masonry-app/src/main/res/mipmap-mdpi/ic_launcher.webp
deleted file mode 100644
index 4f0f1d6..0000000
Binary files a/masonry-app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ
diff --git a/masonry-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/masonry-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
deleted file mode 100644
index 62b611d..0000000
Binary files a/masonry-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/masonry-app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/masonry-app/src/main/res/mipmap-xhdpi/ic_launcher.webp
deleted file mode 100644
index 948a307..0000000
Binary files a/masonry-app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ
diff --git a/masonry-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/masonry-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
deleted file mode 100644
index 1b9a695..0000000
Binary files a/masonry-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/masonry-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/masonry-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
deleted file mode 100644
index 28d4b77..0000000
Binary files a/masonry-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/masonry-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/masonry-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9287f50..0000000
Binary files a/masonry-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/masonry-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/masonry-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
deleted file mode 100644
index aa7d642..0000000
Binary files a/masonry-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/masonry-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/masonry-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9126ae3..0000000
Binary files a/masonry-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/masonry-app/src/main/res/values/styles.xml b/masonry-app/src/main/res/values/styles.xml
deleted file mode 100644
index a4bfa92..0000000
--- a/masonry-app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/masonry-demo/Cargo.toml b/masonry-demo/Cargo.toml
deleted file mode 100644
index 24c07cc..0000000
--- a/masonry-demo/Cargo.toml
+++ /dev/null
@@ -1,21 +0,0 @@
-[package]
-name = "android-view-masonry-demo"
-version = "0.1.0"
-edition = "2024"
-
-[lib]
-name = "main"
-crate-type = ["cdylib"]
-
-[dependencies]
-android-view = { path = ".." }
-masonry = { git = "https://github.com/linebender/xilem" }
-masonry_android = { path = "../masonry" }
-
-# Send tracing events to Android GPU inspector, for profiling
-tracing_android_trace = "0.1.1"
-tracing-subscriber = "0.3.19"
-# Make events recorded with profiling (e.g. in wgpu) visible to Android GPU inspector
-profiling = { version = "1.0.16", features = ["profile-with-tracing"] }
-# Make events recorded to `tracing` visible in logcat
-tracing = { version = "0.1.38", features = ["log-always"] }
diff --git a/masonry-demo/src/lib.rs b/masonry-demo/src/lib.rs
deleted file mode 100644
index caaf52a..0000000
--- a/masonry-demo/src/lib.rs
+++ /dev/null
@@ -1,108 +0,0 @@
-// Copyright 2024 the Xilem Authors
-// SPDX-License-Identifier: Apache-2.0
-
-#![deny(unsafe_op_in_unsafe_fn)]
-
-use android_view::{
- jni::{
- JNIEnv, JavaVM,
- sys::{JNI_VERSION_1_6, JavaVM as RawJavaVM, jint, jlong},
- },
- *,
-};
-use masonry::{
- core::{ErasedAction, NewWidget, Properties, Widget, WidgetId},
- properties::Padding,
- theme::default_property_set,
- widgets::{Button, ButtonPress, Flex, Label, Portal, TextAction, TextArea, TextInput},
-};
-use masonry_android::{AppDriver, DriverCtx};
-use std::{ffi::c_void, sync::Arc};
-use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
-
-const WIDGET_SPACING: f64 = 5.0;
-
-struct Driver {
- next_task: String,
-}
-
-impl AppDriver for Driver {
- fn on_action(&mut self, ctx: &mut DriverCtx<'_>, _widget_id: WidgetId, action: ErasedAction) {
- if action.is::() {
- ctx.render_root().edit_root_widget(|mut root| {
- let mut portal = root.downcast::>();
- let mut flex = Portal::child_mut(&mut portal);
- Flex::add_child(&mut flex, Label::new(self.next_task.clone()).with_auto_id());
-
- let mut first_row = Flex::child_mut(&mut flex, 0).unwrap();
- let mut first_row = first_row.downcast::();
- let mut text_input = Flex::child_mut(&mut first_row, 0).unwrap();
- let mut text_input = text_input.downcast::();
- let mut text_area = TextInput::text_mut(&mut text_input);
- TextArea::reset_text(&mut text_area, "");
- });
- } else if action.is::() {
- let action = action.downcast::().unwrap();
- match *action {
- TextAction::Changed(new_text) => {
- self.next_task = new_text.clone();
- }
- TextAction::Entered(_) => {}
- }
- }
- }
-}
-
-fn make_widget_tree() -> impl Widget {
- Portal::new(
- Flex::column()
- .with_child(NewWidget::new_with_props(
- Flex::row()
- .with_flex_child(TextInput::new("").with_auto_id(), 1.0)
- .with_child(Button::new("Add task").with_auto_id()),
- Properties::new().with(Padding::all(WIDGET_SPACING)),
- ))
- .with_spacer(WIDGET_SPACING)
- .with_auto_id(),
- )
-}
-
-extern "system" fn new_view_peer<'local>(
- mut env: JNIEnv<'local>,
- _view: View<'local>,
- context: Context<'local>,
-) -> jlong {
- masonry_android::new_view_peer(
- &mut env,
- &context,
- NewWidget::new(make_widget_tree()).erased(),
- Driver {
- next_task: String::new(),
- },
- Arc::new(default_property_set()),
- )
-}
-
-/// Symbol run at JNI load time.
-///
-/// # Safety
-/// There is no alternative, interacting with JNI is always unsafe at some level.
-#[unsafe(no_mangle)]
-pub unsafe extern "system" fn JNI_OnLoad(vm: *mut RawJavaVM, _: *mut c_void) -> jint {
- // This will try to create a "log" logger, and error because one was already created above
- // We therefore ignore the error
- // Ideally, we'd only ignore the SetLoggerError, but the only way that's possible is to inspect
- // `Debug/Display` on the TryInitError, which is awful.
- let _ = tracing_subscriber::registry()
- .with(tracing_android_trace::AndroidTraceLayer::new())
- .try_init();
-
- let vm = unsafe { JavaVM::from_raw(vm) }.unwrap();
- let mut env = vm.get_env().unwrap();
- register_view_class(
- &mut env,
- "org/linebender/android/masonrydemo/DemoView",
- new_view_peer,
- );
- JNI_VERSION_1_6
-}
diff --git a/masonry/Cargo.toml b/masonry/Cargo.toml
deleted file mode 100644
index 0362ef1..0000000
--- a/masonry/Cargo.toml
+++ /dev/null
@@ -1,11 +0,0 @@
-[package]
-name = "masonry_android"
-version = "0.1.0"
-edition = "2024"
-
-[dependencies]
-accesskit_android = "0.4.0"
-android-view = { path = ".." }
-masonry_core = { git = "https://github.com/linebender/xilem" }
-pollster = "0.4.0"
-tracing = "0.1.40"
diff --git a/masonry/src/app_driver.rs b/masonry/src/app_driver.rs
deleted file mode 100644
index bb78782..0000000
--- a/masonry/src/app_driver.rs
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright 2025 the Xilem Authors
-// SPDX-License-Identifier: Apache-2.0
-
-use masonry_core::{
- app::RenderRoot,
- core::{ErasedAction, WidgetId},
-};
-
-use crate::MasonryState;
-
-/// Context for the [`AppDriver`] trait.
-///
-/// Currently holds a reference to the [`RenderRoot`].
-pub struct DriverCtx<'a> {
- // We make no guarantees about the fields of this struct, but
- // they must all be public so that the type can be constructed
- // externally.
- // This is needed for external users, whilst our external API
- // is not yet designed.
- #[doc(hidden)]
- pub render_root: &'a mut RenderRoot,
-}
-
-/// A trait for defining how your app interacts with the Masonry widget tree.
-///
-/// When launching your app with [`crate::app::run`], you need to provide
-/// a type that implements this trait.
-pub trait AppDriver {
- /// A hook which will be executed when a widget emits an [`Action`].
- fn on_action(&mut self, ctx: &mut DriverCtx<'_>, widget_id: WidgetId, action: ErasedAction);
-
- #[expect(unused_variables, reason = "Default impl doesn't use arguments")]
- /// A hook which will be executed when the application starts, to allow initial configuration of the `MasonryState`.
- ///
- /// Use cases include loading fonts.
- fn on_start(&mut self, state: &mut MasonryState) {}
-}
-
-impl DriverCtx<'_> {
- // TODO - Add method to create timer
-
- /// Access the [`RenderRoot`].
- pub fn render_root(&mut self) -> &mut RenderRoot {
- self.render_root
- }
-
- /// Returns `true` if something happened that requires a rewrite pass or a re-render.
- pub fn content_changed(&self) -> bool {
- self.render_root.needs_rewrite_passes()
- }
-}
-
-#[cfg(doctest)]
-/// Doctests aren't collected under `cfg(test)`; we can use `cfg(doctest)` instead
-mod doctests {
- /// ```no_run
- /// use masonry::app::DriverCtx;
- /// let _ctx = DriverCtx {
- /// render_root: unimplemented!()
- /// };
- /// ```
- const _DRIVER_CTX_EXTERNALLY_CONSTRUCTIBLE: () = {};
-}
diff --git a/masonry/src/lib.rs b/masonry/src/lib.rs
deleted file mode 100644
index 4ffeafb..0000000
--- a/masonry/src/lib.rs
+++ /dev/null
@@ -1,612 +0,0 @@
-// Copyright 2024 the Xilem Authors
-// SPDX-License-Identifier: Apache-2.0
-
-use android_view::{
- jni::{
- JNIEnv,
- sys::{jint, jlong},
- },
- ndk::{event::Keycode, native_window::NativeWindow},
- *,
-};
-use masonry_core::{
- accesskit::{ActionHandler, ActionRequest, ActivationHandler, TreeUpdate},
- app::{RenderRoot, RenderRootOptions, RenderRootSignal, WindowSizePolicy},
- core::{DefaultProperties, Handled, NewWidget, TextEvent, Widget, WindowEvent},
- dpi::PhysicalSize,
- peniko::Color,
- util::Instant,
- vello::{
- self, Renderer, RendererOptions, Scene,
- kurbo::Affine,
- util::{RenderContext, RenderSurface},
- wgpu::{
- self, PresentMode,
- rwh::{DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, WindowHandle},
- },
- },
-};
-use std::sync::{
- Arc,
- mpsc::{self, Receiver},
-};
-use tracing::{debug, info, info_span};
-
-mod app_driver;
-pub use app_driver::*;
-
-// From VelloCompose
-struct AndroidWindowHandle {
- window: NativeWindow,
-}
-
-impl HasDisplayHandle for AndroidWindowHandle {
- fn display_handle(&self) -> Result, HandleError> {
- Ok(DisplayHandle::android())
- }
-}
-
-impl HasWindowHandle for AndroidWindowHandle {
- fn window_handle(&self) -> Result, HandleError> {
- self.window.window_handle()
- }
-}
-
-/// Helper function that creates a vello `Renderer` for a given `RenderContext` and `RenderSurface`
-fn create_vello_renderer(render_cx: &RenderContext, surface: &RenderSurface<'_>) -> Renderer {
- Renderer::new(
- &render_cx.devices[surface.dev_id].device,
- RendererOptions {
- use_cpu: false,
- antialiasing_support: vello::AaSupport::area_only(),
- num_init_threads: None,
- // TODO: add pipeline cache.
- pipeline_cache: None,
- },
- )
- .expect("Couldn't create renderer")
-}
-
-fn scale_factor<'local>(env: &mut JNIEnv<'local>, android_ctx: &Context<'local>) -> f64 {
- let res = android_ctx.resources(env);
- let metrics = res.display_metrics(env);
- metrics.density(env) as f64
-}
-
-fn show_soft_input<'local>(env: &mut JNIEnv<'local>, view: &View<'local>) {
- let imm = view.input_method_manager(env);
- imm.show_soft_input(env, view, 0);
-}
-
-fn hide_soft_input<'local>(env: &mut JNIEnv<'local>, view: &View<'local>) {
- let imm = view.input_method_manager(env);
- let window_token = view.window_token(env);
- imm.hide_soft_input_from_window(env, &window_token, 0);
-}
-
-pub struct MasonryState {
- render_cx: RenderContext,
- render_root: RenderRoot,
- signal_receiver: Receiver,
- tap_counter: TapCounter,
- renderer: Option,
- render_surface: Option>,
- // Is `Some` if the most recently displayed frame was an animation frame.
- last_anim: Option,
- accesskit_adapter: accesskit_android::Adapter,
-}
-
-impl MasonryState {
- pub fn new(
- root_widget: NewWidget,
- default_properties: Arc,
- scale_factor: f64,
- ) -> Self {
- let render_cx = RenderContext::new();
- let (signal_sender, signal_receiver) = mpsc::channel();
-
- Self {
- render_cx,
- render_root: RenderRoot::new(
- root_widget,
- move |signal| {
- signal_sender.send(signal).unwrap();
- },
- RenderRootOptions {
- default_properties,
- use_system_fonts: true,
- size_policy: WindowSizePolicy::User,
- scale_factor,
- test_font: None,
- },
- ),
- signal_receiver,
- renderer: None,
- tap_counter: TapCounter::default(),
- render_surface: None,
- last_anim: None,
- accesskit_adapter: Default::default(),
- }
- }
-}
-
-#[derive(Default)]
-struct MasonryAccessActivationHandler {
- requested_initial_tree: bool,
-}
-
-impl ActivationHandler for MasonryAccessActivationHandler {
- fn request_initial_tree(&mut self) -> Option {
- self.requested_initial_tree = true;
- None
- }
-}
-
-struct MasonryAccessActionHandler<'a> {
- render_root: &'a mut RenderRoot,
-}
-
-impl ActionHandler for MasonryAccessActionHandler<'_> {
- fn do_action(&mut self, request: ActionRequest) {
- self.render_root.handle_access_event(request);
- }
-}
-
-struct MasonryViewPeer {
- state: MasonryState,
- app_driver: Driver,
-}
-
-impl MasonryViewPeer {
- fn handle_signals(&mut self, ctx: &mut CallbackCtx) {
- let mut needs_redraw = false;
- while let Ok(signal) = self.state.signal_receiver.try_recv() {
- match signal {
- RenderRootSignal::Action(action, widget_id) => {
- let mut driver_ctx = DriverCtx {
- render_root: &mut self.state.render_root,
- };
- debug!("Action {:?} on widget {:?}", action, widget_id);
- self.app_driver
- .on_action(&mut driver_ctx, widget_id, action);
- }
- RenderRootSignal::StartIme => {
- ctx.push_static_deferred_callback(show_soft_input);
- }
- RenderRootSignal::EndIme => {
- ctx.push_static_deferred_callback(hide_soft_input);
- }
- RenderRootSignal::ImeMoved(_position, _size) => {
- // TODO
- }
- RenderRootSignal::RequestRedraw => {
- needs_redraw = true;
- }
- RenderRootSignal::RequestAnimFrame => {
- // Does this need to do something different from RequestRedraw?
- needs_redraw = true;
- }
- RenderRootSignal::TakeFocus => {
- // TODO
- }
- RenderRootSignal::SetCursor(_cursor) => {
- // TODO?
- }
- RenderRootSignal::SetSize(_size) => {
- // TODO: Does this ever apply, maybe for embedded views?
- }
- RenderRootSignal::SetTitle(_title) => {
- // TODO: Does this ever apply?
- }
- RenderRootSignal::DragWindow => {
- // TODO: Does this ever apply?
- }
- RenderRootSignal::DragResizeWindow(_direction) => {
- // TODO: Does this ever apply?
- }
- RenderRootSignal::ToggleMaximized => {
- // TODO: Does this ever apply?
- }
- RenderRootSignal::Minimize => {
- // TODO: Does this ever apply?
- }
- RenderRootSignal::Exit => {
- // TODO: Should we do something with this?
- }
- RenderRootSignal::ShowWindowMenu(_position) => {
- // TODO: Does this ever apply?
- }
- RenderRootSignal::WidgetSelectedInInspector(widget_id) => {
- let Some(widget) = self.state.render_root.get_widget(widget_id) else {
- return;
- };
- let widget_name = widget.short_type_name();
- let display_name = if let Some(debug_text) = widget.get_debug_text() {
- format!("{widget_name}<{debug_text}>")
- } else {
- widget_name.into()
- };
- info!("Widget selected in inspector: {widget_id} - {display_name}");
- }
- }
- }
-
- // If we're processing a lot of actions, we may have a lot of pending redraws.
- // We batch them up to avoid redundant requests.
- if needs_redraw && self.state.render_surface.is_some() {
- ctx.view.post_frame_callback(&mut ctx.env);
- }
- }
-
- fn redraw(&mut self, ctx: &mut CallbackCtx) {
- let _span = info_span!("redraw");
-
- let (scene, tree_update) = self.state.render_root.redraw();
-
- if let Some(events) = self
- .state
- .accesskit_adapter
- .update_if_active(|| tree_update)
- {
- ctx.push_dynamic_deferred_callback(move |env, view| {
- events.raise(env, &view.0);
- });
- }
-
- let android_ctx = ctx.view.context(&mut ctx.env);
- let scale_factor = scale_factor(&mut ctx.env, &android_ctx);
- let scene = if scale_factor == 1.0 {
- scene
- } else {
- let mut new_scene = Scene::new();
- new_scene.append(&scene, Some(Affine::scale(scale_factor)));
- new_scene
- };
-
- // Get the RenderSurface (surface + config).
- let surface = self.state.render_surface.as_ref().unwrap();
-
- // Get the window size.
- let width = surface.config.width;
- let height = surface.config.height;
-
- // Get a handle to the device.
- let device_handle = &self.state.render_cx.devices[surface.dev_id];
-
- // Render to the surface's texture.
- self.state
- .renderer
- .as_mut()
- .unwrap()
- .render_to_texture(
- &device_handle.device,
- &device_handle.queue,
- &scene,
- &surface.target_view,
- &vello::RenderParams {
- base_color: Color::BLACK,
- width,
- height,
- antialiasing_method: vello::AaConfig::Area,
- },
- )
- .expect("failed to render to surface");
-
- // Get the surface's texture.
- let surface_texture = surface
- .surface
- .get_current_texture()
- .expect("failed to get surface texture");
-
- // Perform the copy.
- let mut encoder =
- device_handle
- .device
- .create_command_encoder(&wgpu::CommandEncoderDescriptor {
- label: Some("Surface Blit"),
- });
- surface.blitter.copy(
- &device_handle.device,
- &mut encoder,
- &surface.target_view,
- &surface_texture
- .texture
- .create_view(&wgpu::TextureViewDescriptor::default()),
- );
- device_handle.queue.submit([encoder.finish()]);
- // Queue the texture to be presented on the surface.
- surface_texture.present();
-
- device_handle.device.poll(wgpu::Maintain::Poll);
- }
-
- fn on_key_event<'local>(
- &mut self,
- ctx: &mut CallbackCtx<'local>,
- event: &KeyEvent<'local>,
- ) -> bool {
- let handled = self
- .state
- .render_root
- .handle_text_event(TextEvent::Keyboard(event.to_keyboard_event(&mut ctx.env)));
- self.handle_signals(ctx);
- matches!(handled, Handled::Yes)
- }
-
- fn with_access_activation_handler<'local, T>(
- &mut self,
- ctx: &mut CallbackCtx<'local>,
- f: impl FnOnce(
- &mut CallbackCtx<'local>,
- &mut accesskit_android::Adapter,
- &mut MasonryAccessActivationHandler,
- ) -> T,
- ) -> T {
- let mut handler = MasonryAccessActivationHandler::default();
- let result = f(ctx, &mut self.state.accesskit_adapter, &mut handler);
- if handler.requested_initial_tree {
- self.state
- .render_root
- .handle_window_event(WindowEvent::RebuildAccessTree);
- self.handle_signals(ctx);
- }
- result
- }
-}
-
-impl ViewPeer for MasonryViewPeer {
- fn on_key_down<'local>(
- &mut self,
- ctx: &mut CallbackCtx<'local>,
- _: Keycode,
- event: &KeyEvent<'local>,
- ) -> bool {
- self.on_key_event(ctx, event)
- }
-
- fn on_key_up<'local>(
- &mut self,
- ctx: &mut CallbackCtx<'local>,
- _: Keycode,
- event: &KeyEvent<'local>,
- ) -> bool {
- self.on_key_event(ctx, event)
- }
-
- fn on_touch_event<'local>(
- &mut self,
- ctx: &mut CallbackCtx<'local>,
- event: &MotionEvent<'local>,
- ) -> bool {
- let Some(ev) = event.to_pointer_event(&mut ctx.env, &self.state.tap_counter.vc) else {
- return false;
- };
- let ev = self.state.tap_counter.attach_count(ev);
- self.state.render_root.handle_pointer_event(ev);
- self.handle_signals(ctx);
- true
- }
-
- fn on_generic_motion_event<'local>(
- &mut self,
- ctx: &mut CallbackCtx<'local>,
- event: &MotionEvent<'local>,
- ) -> bool {
- self.on_touch_event(ctx, event)
- }
-
- fn on_hover_event<'local>(
- &mut self,
- ctx: &mut CallbackCtx<'local>,
- event: &MotionEvent<'local>,
- ) -> bool {
- let action = event.action(&mut ctx.env);
- let x = event.x(&mut ctx.env);
- let y = event.y(&mut ctx.env);
- if let Some(events) = self.with_access_activation_handler(ctx, |_ctx, adapter, handler| {
- adapter.on_hover_event(handler, action, x, y)
- }) {
- ctx.push_dynamic_deferred_callback(move |env, view| {
- events.raise(env, &view.0);
- });
- true
- } else {
- self.on_touch_event(ctx, event)
- }
- }
-
- fn on_focus_changed<'local>(
- &mut self,
- ctx: &mut CallbackCtx<'local>,
- gain_focus: bool,
- _direction: jint,
- _previously_focused_rect: Option<&Rect<'local>>,
- ) {
- self.state
- .render_root
- .handle_text_event(TextEvent::WindowFocusChange(gain_focus));
- self.handle_signals(ctx);
- }
-
- fn surface_changed<'local>(
- &mut self,
- ctx: &mut CallbackCtx<'local>,
- holder: &SurfaceHolder<'local>,
- _format: jint,
- width: jint,
- height: jint,
- ) {
- self.state.tap_counter = TapCounter::new(ctx.view.view_configuration(&mut ctx.env));
- let android_ctx = ctx.view.context(&mut ctx.env);
- let scale_factor = scale_factor(&mut ctx.env, &android_ctx);
- self.state
- .render_root
- .handle_window_event(WindowEvent::Rescale(scale_factor));
- let size = PhysicalSize {
- width: width as u32,
- height: height as u32,
- };
- self.state
- .render_root
- .handle_window_event(WindowEvent::Resize(size));
- self.handle_signals(ctx);
-
- let window = holder.surface(&mut ctx.env).to_native_window(&mut ctx.env);
- // Drop the old surface, if any, that owned the native window
- // before creating a new one. Otherwise, we crash with
- // ERROR_NATIVE_WINDOW_IN_USE_KHR.
- self.state.render_surface = None;
- let surface = self
- .state
- .render_cx
- .instance
- .create_surface(wgpu::SurfaceTarget::from(AndroidWindowHandle { window }))
- .expect("Error creating surface");
- let dev_id = pollster::block_on(self.state.render_cx.device(Some(&surface)))
- .expect("No compatible device");
- let device_handle = &self.state.render_cx.devices[dev_id];
- let capabilities = surface.get_capabilities(device_handle.adapter());
- let present_mode = if capabilities.present_modes.contains(&PresentMode::Mailbox) {
- PresentMode::Mailbox
- } else {
- PresentMode::AutoVsync
- };
-
- let surface_future = self.state.render_cx.create_render_surface(
- surface,
- width as _,
- height as _,
- present_mode,
- );
- let surface = pollster::block_on(surface_future).expect("Error creating surface");
-
- // Create a vello Renderer for the surface (using its device id)
- self.state
- .renderer
- .get_or_insert_with(|| create_vello_renderer(&self.state.render_cx, &surface));
- self.state.render_surface = Some(surface);
-
- self.redraw(ctx);
- }
-
- fn surface_destroyed<'local>(
- &mut self,
- ctx: &mut CallbackCtx<'local>,
- _holder: &SurfaceHolder<'local>,
- ) {
- self.state.render_surface = None;
- ctx.view.remove_frame_callback(&mut ctx.env);
- }
-
- fn do_frame(&mut self, ctx: &mut CallbackCtx, _frame_time_nanos: jlong) {
- let _span = info_span!("do_frame");
-
- let now = Instant::now();
- // TODO: this calculation uses wall-clock time of the paint call, which
- // potentially has jitter.
- //
- // See https://github.com/linebender/druid/issues/85 for discussion.
- let last = self.state.last_anim.take();
- let elapsed = last.map(|t| now.duration_since(t)).unwrap_or_default();
- self.state
- .render_root
- .handle_window_event(WindowEvent::AnimFrame(elapsed));
-
- // Make sure we handle any signals emitted in response to the
- // `AnimFrame` event before we redraw.
- self.handle_signals(ctx);
-
- // If this animation will continue, store the time.
- // If a new animation starts, then it will have zero reported elapsed time.
- let animation_continues = self.state.render_root.needs_anim();
- self.state.last_anim = animation_continues.then_some(now);
-
- self.redraw(ctx);
- }
-
- fn as_accessibility_node_provider(&mut self) -> Option<&mut dyn AccessibilityNodeProvider> {
- Some(self)
- }
-
- fn as_input_connection(&mut self) -> Option<&mut dyn InputConnection> {
- // TODO
- None
- }
-}
-
-impl AccessibilityNodeProvider for MasonryViewPeer {
- fn create_accessibility_node_info<'local>(
- &mut self,
- ctx: &mut CallbackCtx<'local>,
- virtual_view_id: jint,
- ) -> AccessibilityNodeInfo<'local> {
- self.with_access_activation_handler(ctx, |ctx, adapter, handler| {
- AccessibilityNodeInfo(adapter.create_accessibility_node_info(
- handler,
- &mut ctx.env,
- &ctx.view.0,
- virtual_view_id,
- ))
- })
- }
-
- fn find_focus<'local>(
- &mut self,
- ctx: &mut CallbackCtx<'local>,
- focus_type: jint,
- ) -> AccessibilityNodeInfo<'local> {
- self.with_access_activation_handler(ctx, |ctx, adapter, handler| {
- AccessibilityNodeInfo(adapter.find_focus(
- handler,
- &mut ctx.env,
- &ctx.view.0,
- focus_type,
- ))
- })
- }
-
- fn perform_action<'local>(
- &mut self,
- ctx: &mut CallbackCtx<'local>,
- virtual_view_id: jint,
- action: jint,
- arguments: &Bundle<'local>,
- ) -> bool {
- let Some(action) =
- accesskit_android::PlatformAction::from_java(&mut ctx.env, action, &arguments.0)
- else {
- return false;
- };
- let mut action_handler = MasonryAccessActionHandler {
- render_root: &mut self.state.render_root,
- };
- if let Some(events) = self.state.accesskit_adapter.perform_action(
- &mut action_handler,
- virtual_view_id,
- &action,
- ) {
- ctx.push_dynamic_deferred_callback(move |env, view| {
- events.raise(env, &view.0);
- });
- self.handle_signals(ctx);
- true
- } else {
- false
- }
- }
-}
-
-// TODO: InputConnection
-
-pub fn new_view_peer<'local>(
- env: &mut JNIEnv<'local>,
- android_ctx: &Context<'local>,
- root_widget: NewWidget,
- mut app_driver: impl AppDriver + 'static,
- default_properties: Arc,
-) -> jlong {
- let scale_factor = scale_factor(env, android_ctx);
- let mut state = MasonryState::new(root_widget, default_properties, scale_factor);
- app_driver.on_start(&mut state);
- register_view_peer(MasonryViewPeer { state, app_driver })
-}
diff --git a/settings.gradle b/settings.gradle
index e059924..9e81faa 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -15,4 +15,3 @@ dependencyResolutionManagement {
include ':app'
include ':library'
-include ':masonry-app'
diff --git a/src/callback_ctx.rs b/src/callback_ctx.rs
index cd26bd8..1d94cde 100644
--- a/src/callback_ctx.rs
+++ b/src/callback_ctx.rs
@@ -3,9 +3,11 @@ use smallvec::SmallVec;
use crate::view::View;
+type DynamicCallback<'local> = Box, &View<'local>)>;
+
enum DeferredCallback<'local> {
Static(fn(&mut JNIEnv<'local>, &View<'local>)),
- Dynamic(Box, &View<'local>)>),
+ Dynamic(DynamicCallback<'local>),
}
pub struct CallbackCtx<'local> {
diff --git a/src/events.rs b/src/events.rs
index fc6810b..652c1aa 100644
--- a/src/events.rs
+++ b/src/events.rs
@@ -12,7 +12,10 @@ use num_enum::FromPrimitive;
use ui_events::{
ScrollDelta,
keyboard::{KeyboardEvent, Modifiers},
- pointer::{ContactGeometry, PointerEvent, PointerId, PointerState, PointerUpdate},
+ pointer::{
+ ContactGeometry, PointerButtonEvent, PointerEvent, PointerId, PointerScrollEvent,
+ PointerState, PointerUpdate,
+ },
};
use crate::ViewConfiguration;
@@ -425,9 +428,11 @@ impl<'local> MotionEvent<'local> {
let contact_geometry = if pointer.pointer_type == PointerType::Touch {
let height = self.axis(env, Axis::TouchMajor, action_index) as f64;
let width = self.axis(env, Axis::TouchMinor, action_index) as f64;
- (height > 0.0 && width > 0.0)
- .then_some(ContactGeometry { width, height })
- .unwrap_or_default()
+ if height > 0.0 && width > 0.0 {
+ ContactGeometry { width, height }
+ } else {
+ ContactGeometry::default()
+ }
} else {
Default::default()
};
@@ -445,6 +450,11 @@ impl<'local> MotionEvent<'local> {
orientation,
pressure: self.axis(env, Axis::Pressure, action_index) * 0.5,
tangential_pressure: 0.0,
+ scale_factor: if vc.scale_factor.is_finite() && vc.scale_factor > 0.0 {
+ vc.scale_factor
+ } else {
+ 1.0
+ },
};
let button = {
@@ -467,16 +477,18 @@ impl<'local> MotionEvent<'local> {
};
Some(match action {
- MotionAction::Down | MotionAction::PointerDown => PointerEvent::Down {
- pointer,
- state,
- button,
- },
- MotionAction::Up | MotionAction::PointerUp => PointerEvent::Up {
+ MotionAction::Down | MotionAction::PointerDown => {
+ PointerEvent::Down(PointerButtonEvent {
+ pointer,
+ state,
+ button,
+ })
+ }
+ MotionAction::Up | MotionAction::PointerUp => PointerEvent::Up(PointerButtonEvent {
pointer,
state,
button,
- },
+ }),
MotionAction::Move | MotionAction::HoverMove => {
let hsz = self.history_size(env);
let mut coalesced: Vec = vec![state.clone(); hsz as usize];
@@ -492,9 +504,11 @@ impl<'local> MotionEvent<'local> {
self.historical_axis(env, Axis::TouchMajor, action_index, pos) as f64;
let width =
self.historical_axis(env, Axis::TouchMinor, action_index, pos) as f64;
- (height > 0.0 && width > 0.0)
- .then_some(ContactGeometry { width, height })
- .unwrap_or_default()
+ if height > 0.0 && width > 0.0 {
+ ContactGeometry { width, height }
+ } else {
+ ContactGeometry::default()
+ }
} else {
Default::default()
};
@@ -527,7 +541,7 @@ impl<'local> MotionEvent<'local> {
MotionAction::Cancel => PointerEvent::Cancel(pointer),
MotionAction::HoverEnter => PointerEvent::Enter(pointer),
MotionAction::HoverExit => PointerEvent::Leave(pointer),
- MotionAction::Scroll => PointerEvent::Scroll {
+ MotionAction::Scroll => PointerEvent::Scroll(PointerScrollEvent {
pointer,
delta: ScrollDelta::PixelDelta(PhysicalPosition:: {
x: (self.axis(env, Axis::Hscroll, action_index)
@@ -536,7 +550,7 @@ impl<'local> MotionEvent<'local> {
* vc.scaled_vertical_scroll_factor) as f64,
}),
state,
- },
+ }),
_ => {
// Other current `MotionAction` values relate to gamepad/joystick buttons;
// ui-events doesn't currently have types for these, so consider them unhandled.
@@ -620,11 +634,11 @@ impl TapCounter {
///
pub fn attach_count(&mut self, e: PointerEvent) -> PointerEvent {
match e {
- PointerEvent::Down {
+ PointerEvent::Down(PointerButtonEvent {
button,
pointer,
state,
- } => {
+ }) => {
let e = if let Some(i) =
self.taps.iter().position(|TapState { x, y, up_time, .. }| {
let dx = (x - state.position.x).abs();
@@ -641,11 +655,11 @@ impl TapCounter {
self.taps[i].x = state.position.x;
self.taps[i].y = state.position.y;
- PointerEvent::Down {
+ PointerEvent::Down(PointerButtonEvent {
button,
pointer,
state: PointerState { count, ..state },
- }
+ })
} else {
let s = TapState {
pointer_id: pointer.pointer_id,
@@ -656,34 +670,34 @@ impl TapCounter {
y: state.position.y,
};
self.taps.push(s);
- PointerEvent::Down {
+ PointerEvent::Down(PointerButtonEvent {
button,
pointer,
state: PointerState { count: 1, ..state },
- }
+ })
};
self.clear_expired(state.time);
e
}
- PointerEvent::Up {
+ PointerEvent::Up(PointerButtonEvent {
button,
pointer,
ref state,
- } => {
+ }) => {
if let Some(i) = self
.taps
.iter()
.position(|TapState { pointer_id, .. }| *pointer_id == pointer.pointer_id)
{
self.taps[i].up_time = state.time;
- PointerEvent::Up {
+ PointerEvent::Up(PointerButtonEvent {
button,
pointer,
state: PointerState {
count: self.taps[i].count,
..state.clone()
},
- }
+ })
} else {
e.clone()
}
@@ -717,13 +731,17 @@ impl TapCounter {
},
coalesced: coalesced
.iter()
- .cloned()
- .map(|u| PointerState { count, ..u })
+ .map(|state| PointerState {
+ count,
+ ..state.clone()
+ })
.collect(),
predicted: predicted
.iter()
- .cloned()
- .map(|u| PointerState { count, ..u })
+ .map(|state| PointerState {
+ count,
+ ..state.clone()
+ })
.collect(),
})
} else {
@@ -735,7 +753,9 @@ impl TapCounter {
.retain(|TapState { pointer_id, .. }| *pointer_id != p.pointer_id);
e.clone()
}
- PointerEvent::Enter(..) | PointerEvent::Scroll { .. } => e.clone(),
+ PointerEvent::Enter(..) | PointerEvent::Scroll(..) | PointerEvent::Gesture(..) => {
+ e.clone()
+ }
}
}
diff --git a/src/ime.rs b/src/ime.rs
index 508bab7..d077020 100644
--- a/src/ime.rs
+++ b/src/ime.rs
@@ -7,6 +7,9 @@ use std::borrow::Cow;
use crate::{binder::*, callback_ctx::*, events::KeyEvent, util::*, view::*};
+mod text;
+pub use text::*;
+
pub const INPUT_TYPE_MASK_CLASS: u32 = 0x0000000f;
pub const INPUT_TYPE_MASK_VARIATION: u32 = 0x00000ff0;
pub const INPUT_TYPE_MASK_FLAGS: u32 = 0x00fff000;
diff --git a/src/ime/text.rs b/src/ime/text.rs
new file mode 100644
index 0000000..fda6134
--- /dev/null
+++ b/src/ime/text.rs
@@ -0,0 +1,455 @@
+// Copyright 2026 the UI Events Authors
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+//! Value adapters between Android [`InputConnection`] and `ui-events`.
+//!
+//! This module owns Android-specific value translation at the existing native
+//! input-connection boundary:
+//!
+//! - mutating `InputConnection` calls become [`TextInputEvent`] values;
+//! - [`SurroundingTextProvider`] becomes Android UTF-16 surrounding text; and
+//! - coherent host snapshots become selection/composition updates.
+//!
+//! The parent `ime` module continues to own the Java object, JNI callbacks,
+//! connection validity, thread affinity, nested batch-edit lifetime, styled
+//! `CharSequence` handling, and `InputMethodManager` notifications.
+//!
+//! Android calls Java UTF-16 code units “characters” in most `InputConnection`
+//! methods. Its explicit code-point deletion method is kept separate here.
+//!
+//! [`InputConnection`]: super::InputConnection
+//! [`SurroundingTextProvider`]: ui_text_input::SurroundingTextProvider
+//! [`TextInputEvent`]: ui_events::text::TextInputEvent
+
+use std::string::{String, ToString};
+
+use ui_events::text::{
+ CompositionState, TextCursorAnchor, TextCursorOffset, TextCursorPlacement, TextInputAction,
+ TextInputEvent, TextInsertEvent, TextRangeEncoding, TextTargetRange,
+};
+use ui_text_input::{
+ SurroundingTextProvider, SurroundingTextRequest, TextInputHost, TextRangeConverter,
+};
+
+/// Plain values accepted by Android's API-31 `SurroundingText` constructor.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct AndroidSurroundingText {
+ revision: u64,
+ text: String,
+ selection_start: i32,
+ selection_end: i32,
+ offset: i32,
+}
+
+impl AndroidSurroundingText {
+ /// Return the coherent host revision used for this response.
+ pub const fn revision(&self) -> u64 {
+ self.revision
+ }
+
+ /// Return the plain surrounding text.
+ pub fn text(&self) -> &str {
+ &self.text
+ }
+
+ /// Return directional selection start relative to [`Self::text`].
+ pub const fn selection_start(&self) -> i32 {
+ self.selection_start
+ }
+
+ /// Return directional selection end relative to [`Self::text`].
+ pub const fn selection_end(&self) -> i32 {
+ self.selection_end
+ }
+
+ /// Return the absolute UTF-16 offset of the surrounding text's first unit.
+ pub const fn offset(&self) -> i32 {
+ self.offset
+ }
+}
+
+/// Query plain surrounding text for Android `InputConnection`.
+///
+/// `before_length` and `after_length` are Java UTF-16 code units. The host must
+/// return a UTF-16 snapshot containing the complete directional selection.
+/// `max_utf8_bytes` lets the JNI layer impose a payload budget.
+pub fn surrounding_text_from_host(
+ host: &(impl SurroundingTextProvider + ?Sized),
+ before_length: i32,
+ after_length: i32,
+ max_utf8_bytes: u32,
+) -> Option {
+ let before_length = u32::try_from(before_length).ok()?;
+ let after_length = u32::try_from(after_length).ok()?;
+ let surrounding = host.surrounding_text(SurroundingTextRequest::new(
+ before_length,
+ after_length,
+ TextRangeEncoding::Utf16CodeUnits,
+ max_utf8_bytes,
+ ))?;
+ let text = surrounding.text();
+ if text.range().encoding != TextRangeEncoding::Utf16CodeUnits
+ || text.text().len() > max_utf8_bytes as usize
+ {
+ return None;
+ }
+ Some(AndroidSurroundingText {
+ revision: surrounding.revision(),
+ text: text.text().to_string(),
+ selection_start: i32::try_from(surrounding.relative_anchor()).ok()?,
+ selection_end: i32::try_from(surrounding.relative_active()).ok()?,
+ offset: i32::try_from(text.range().range.start).ok()?,
+ })
+}
+
+/// Selection and composing offsets for `InputMethodManager.updateSelection`.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub struct AndroidSelectionUpdate {
+ /// Directional selection start in absolute UTF-16 code units.
+ pub selection_start: i32,
+ /// Directional selection end in absolute UTF-16 code units.
+ pub selection_end: i32,
+ /// Composing-range start in UTF-16, or `-1` when there is no composition.
+ pub composing_start: i32,
+ /// Composing-range end in UTF-16, or `-1` when there is no composition.
+ pub composing_end: i32,
+}
+
+/// Convert a coherent host snapshot into Android selection notification values.
+pub fn selection_update_from_host(
+ host: &(impl TextInputHost + TextRangeConverter + ?Sized),
+) -> Option {
+ let snapshot = host.text_input_snapshot();
+ let selection = snapshot.selection()?;
+ let selection_start = convert_offset(
+ host,
+ selection.anchor(),
+ selection.encoding(),
+ TextRangeEncoding::Utf16CodeUnits,
+ )?;
+ let selection_end = convert_offset(
+ host,
+ selection.active(),
+ selection.encoding(),
+ TextRangeEncoding::Utf16CodeUnits,
+ )?;
+ let (composing_start, composing_end) =
+ snapshot.composition().map_or(Some((-1, -1)), |state| {
+ let range = convert_range(host, state.range(), TextRangeEncoding::Utf16CodeUnits)?;
+ Some((
+ i32::try_from(range.range.start).ok()?,
+ i32::try_from(range.range.end).ok()?,
+ ))
+ })?;
+ Some(AndroidSelectionUpdate {
+ selection_start: i32::try_from(selection_start).ok()?,
+ selection_end: i32::try_from(selection_end).ok()?,
+ composing_start,
+ composing_end,
+ })
+}
+
+/// Convert Android `commitText` to a committed text event.
+pub fn commit_text_event(text: impl Into, new_cursor_position: i32) -> TextInputEvent {
+ TextInputEvent::Insert(
+ TextInsertEvent::new(text).with_cursor_placement(cursor_placement(new_cursor_position)),
+ )
+}
+
+/// Convert Android `setComposingText` to a composition snapshot event.
+pub fn set_composing_text_event(
+ text: impl Into,
+ new_cursor_position: i32,
+) -> TextInputEvent {
+ TextInputEvent::CompositionUpdate(
+ CompositionState::new(text).with_cursor_placement(cursor_placement(new_cursor_position)),
+ )
+}
+
+/// Convert Android `replaceText` to a committed replacement event.
+pub fn replace_text_event(
+ start: i32,
+ end: i32,
+ text: impl Into,
+ new_cursor_position: i32,
+) -> Option {
+ let range = ordered_utf16_range(start, end)?;
+ Some(TextInputEvent::Insert(
+ TextInsertEvent::new(text)
+ .with_replacement_range(range)
+ .with_cursor_placement(cursor_placement(new_cursor_position)),
+ ))
+}
+
+/// Convert Android `deleteSurroundingText` lengths to a UTF-16 deletion event.
+pub fn delete_surrounding_text_event(
+ before_length: i32,
+ after_length: i32,
+) -> Option {
+ Some(TextInputEvent::delete_surrounding_utf16_code_units(
+ u32::try_from(before_length).ok()?,
+ u32::try_from(after_length).ok()?,
+ ))
+}
+
+/// Convert Android `deleteSurroundingTextInCodePoints` lengths.
+pub fn delete_surrounding_text_in_code_points_event(
+ before_length: i32,
+ after_length: i32,
+) -> Option {
+ Some(TextInputEvent::delete_surrounding_unicode_code_points(
+ u32::try_from(before_length).ok()?,
+ u32::try_from(after_length).ok()?,
+ ))
+}
+
+/// Convert Android `setSelection` to an ordered UTF-16 selection event.
+pub fn set_selection_event(start: i32, end: i32) -> Option {
+ Some(TextInputEvent::set_selection(ordered_utf16_range(
+ start, end,
+ )?))
+}
+
+/// Convert Android `setComposingRegion` to an ordered UTF-16 region event.
+///
+/// Android defines a zero-sized clipped region as finishing composition, so a
+/// zero-sized input maps to [`TextInputEvent::CompositionEnd`].
+pub fn set_composing_region_event(start: i32, end: i32) -> Option {
+ let range = ordered_utf16_range(start, end)?;
+ if range.range.start == range.range.end {
+ Some(TextInputEvent::CompositionEnd)
+ } else {
+ Some(TextInputEvent::set_composing_region(range))
+ }
+}
+
+/// Convert Android `finishComposingText` to a composition-end event.
+pub const fn finish_composing_text_event() -> TextInputEvent {
+ TextInputEvent::CompositionEnd
+}
+
+/// Convert Android `EditorInfo.IME_ACTION_*` values to portable actions.
+pub const fn editor_action_event(action_id: i32) -> Option {
+ let action = match action_id {
+ 2 => TextInputAction::Go,
+ 3 => TextInputAction::Search,
+ 4 => TextInputAction::Send,
+ 5 => TextInputAction::Next,
+ 6 => TextInputAction::Done,
+ 7 => TextInputAction::Previous,
+ _ => return None,
+ };
+ Some(TextInputEvent::action(action))
+}
+
+fn cursor_placement(new_cursor_position: i32) -> TextCursorPlacement {
+ let (value, relative_to) = if new_cursor_position > 0 {
+ (new_cursor_position - 1, TextCursorAnchor::InsertedTextEnd)
+ } else {
+ (new_cursor_position, TextCursorAnchor::InsertedTextStart)
+ };
+ TextCursorPlacement::Offset(TextCursorOffset::utf16_code_units(value, relative_to))
+}
+
+fn ordered_utf16_range(start: i32, end: i32) -> Option {
+ let start = u32::try_from(start).ok()?;
+ let end = u32::try_from(end).ok()?;
+ Some(TextTargetRange::utf16_code_units(
+ start.min(end),
+ start.max(end),
+ ))
+}
+
+fn convert_offset(
+ host: &(impl TextRangeConverter + ?Sized),
+ offset: u32,
+ source: TextRangeEncoding,
+ destination: TextRangeEncoding,
+) -> Option {
+ let range = target_range(offset, offset, source);
+ Some(convert_range(host, range, destination)?.range.start)
+}
+
+fn convert_range(
+ host: &(impl TextRangeConverter + ?Sized),
+ range: TextTargetRange,
+ destination: TextRangeEncoding,
+) -> Option {
+ if range.encoding == destination {
+ Some(range)
+ } else {
+ host.convert_range(range, destination)
+ }
+}
+
+const fn target_range(start: u32, end: u32, encoding: TextRangeEncoding) -> TextTargetRange {
+ match encoding {
+ TextRangeEncoding::Utf8Bytes => TextTargetRange::utf8_bytes(start, end),
+ TextRangeEncoding::Utf16CodeUnits => TextTargetRange::utf16_code_units(start, end),
+ TextRangeEncoding::UnicodeCodePoints => TextTargetRange::unicode_code_points(start, end),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{
+ AndroidSelectionUpdate, commit_text_event, delete_surrounding_text_event,
+ delete_surrounding_text_in_code_points_event, editor_action_event, replace_text_event,
+ selection_update_from_host, set_composing_region_event, set_composing_text_event,
+ set_selection_event, surrounding_text_from_host,
+ };
+ use ui_events::text::{
+ TextCursorAnchor, TextCursorOffset, TextCursorPlacement, TextInputAction, TextInputEvent,
+ TextRangeEncoding, TextTargetRange,
+ };
+ use ui_text_input::{
+ CompositionSnapshot, SurroundingTextProvider, SurroundingTextRequest,
+ SurroundingTextSnapshot, TextInputHost, TextInputSnapshot, TextRangeConverter,
+ TextRangeSlice, TextSelection,
+ };
+
+ #[derive(Debug)]
+ struct StubHost;
+
+ impl TextInputHost for StubHost {
+ fn text_input_snapshot(&self) -> TextInputSnapshot {
+ TextInputSnapshot::new()
+ .with_revision(3)
+ .with_document_range(TextTargetRange::utf8_bytes(0, 6))
+ .with_selection(TextSelection::utf8_bytes(5, 1))
+ .with_composition(CompositionSnapshot::new(TextTargetRange::utf8_bytes(1, 5)))
+ }
+ }
+
+ impl TextRangeConverter for StubHost {
+ fn convert_range(
+ &self,
+ range: TextTargetRange,
+ encoding: TextRangeEncoding,
+ ) -> Option {
+ if encoding != TextRangeEncoding::Utf16CodeUnits
+ || range.encoding != TextRangeEncoding::Utf8Bytes
+ {
+ return (range.encoding == encoding).then_some(range);
+ }
+ match (range.range.start, range.range.end) {
+ (1, 1) => Some(TextTargetRange::utf16_code_units(1, 1)),
+ (5, 5) => Some(TextTargetRange::utf16_code_units(3, 3)),
+ (1, 5) => Some(TextTargetRange::utf16_code_units(1, 3)),
+ _ => None,
+ }
+ }
+ }
+
+ impl SurroundingTextProvider for StubHost {
+ fn surrounding_text(
+ &self,
+ request: SurroundingTextRequest,
+ ) -> Option {
+ assert_eq!(request.encoding, TextRangeEncoding::Utf16CodeUnits);
+ SurroundingTextSnapshot::new(
+ 3,
+ TextRangeSlice::new("a🙂b", TextTargetRange::utf16_code_units(10, 14))?,
+ TextSelection::utf16_code_units(14, 11),
+ )
+ }
+ }
+
+ #[test]
+ fn surrounding_text_preserves_reversed_utf16_selection_and_offset() {
+ let text = surrounding_text_from_host(&StubHost, 20, 30, 1024).expect("surrounding text");
+ assert_eq!(text.revision(), 3);
+ assert_eq!(text.text(), "a🙂b");
+ assert_eq!(text.selection_start(), 4);
+ assert_eq!(text.selection_end(), 1);
+ assert_eq!(text.offset(), 10);
+ }
+
+ #[test]
+ fn selection_update_converts_direction_and_composition_to_utf16() {
+ assert_eq!(
+ selection_update_from_host(&StubHost),
+ Some(AndroidSelectionUpdate {
+ selection_start: 3,
+ selection_end: 1,
+ composing_start: 1,
+ composing_end: 3,
+ })
+ );
+ }
+
+ #[test]
+ fn cursor_placement_matches_android_relative_rules() {
+ let TextInputEvent::Insert(after) = commit_text_event("🙂", 1) else {
+ panic!("insert event");
+ };
+ assert_eq!(
+ after.cursor_placement,
+ TextCursorPlacement::Offset(TextCursorOffset::utf16_code_units(
+ 0,
+ TextCursorAnchor::InsertedTextEnd
+ ))
+ );
+
+ let TextInputEvent::CompositionUpdate(before) = set_composing_text_event("🙂", -2) else {
+ panic!("composition event");
+ };
+ assert_eq!(
+ before.cursor_placement,
+ TextCursorPlacement::Offset(TextCursorOffset::utf16_code_units(
+ -2,
+ TextCursorAnchor::InsertedTextStart
+ ))
+ );
+ }
+
+ #[test]
+ fn deletion_helpers_keep_code_units_and_code_points_distinct() {
+ let TextInputEvent::DeleteSurrounding(units) =
+ delete_surrounding_text_event(2, 3).expect("valid lengths")
+ else {
+ panic!("delete event");
+ };
+ assert_eq!(units.encoding, TextRangeEncoding::Utf16CodeUnits);
+ let TextInputEvent::DeleteSurrounding(points) =
+ delete_surrounding_text_in_code_points_event(2, 3).expect("valid lengths")
+ else {
+ panic!("delete event");
+ };
+ assert_eq!(points.encoding, TextRangeEncoding::UnicodeCodePoints);
+ assert!(delete_surrounding_text_event(-1, 0).is_none());
+ }
+
+ #[test]
+ fn explicit_ranges_are_nonnegative_and_normalized() {
+ assert_eq!(
+ set_selection_event(7, 3),
+ Some(TextInputEvent::set_selection(
+ TextTargetRange::utf16_code_units(3, 7)
+ ))
+ );
+ assert_eq!(
+ set_composing_region_event(5, 2),
+ Some(TextInputEvent::set_composing_region(
+ TextTargetRange::utf16_code_units(2, 5)
+ ))
+ );
+ assert_eq!(
+ set_composing_region_event(2, 2),
+ Some(TextInputEvent::CompositionEnd)
+ );
+ assert!(set_selection_event(-1, 0).is_none());
+ assert!(replace_text_event(-1, 0, "x", 1).is_none());
+ }
+
+ #[test]
+ fn editor_actions_map_android_constants() {
+ assert_eq!(
+ editor_action_event(3),
+ Some(TextInputEvent::action(TextInputAction::Search))
+ );
+ assert_eq!(editor_action_event(0), None);
+ assert_eq!(editor_action_event(1), None);
+ assert_eq!(editor_action_event(8), None);
+ }
+}
diff --git a/src/view.rs b/src/view.rs
index 53e9c56..191914a 100644
--- a/src/view.rs
+++ b/src/view.rs
@@ -228,8 +228,8 @@ pub trait ViewPeer {
}
static NEXT_PEER_ID: AtomicI64 = AtomicI64::new(0);
-static PEER_MAP: Mutex>>>>> =
- Mutex::new(BTreeMap::new());
+type ViewPeerHandle = SendWrapper>>>;
+static PEER_MAP: Mutex> = Mutex::new(BTreeMap::new());
pub(crate) fn with_peer<'local, F, T: Default>(
env: JNIEnv<'local>,
diff --git a/src/view_configuration.rs b/src/view_configuration.rs
index 43d59a9..fc02a50 100644
--- a/src/view_configuration.rs
+++ b/src/view_configuration.rs
@@ -9,6 +9,8 @@ use jni::{JNIEnv, errors::Error, objects::JObject};
#[derive(Debug, Clone, Default)]
#[allow(dead_code)]
pub struct ViewConfiguration {
+ /// Device density used to convert physical pixels to logical coordinates.
+ pub scale_factor: f64,
/// Milliseconds between the first tap's up event and the
/// subsequent tap's down event to detect a double tap.
pub double_tap_timeout: i32,
@@ -74,7 +76,26 @@ impl ViewConfiguration {
)?
.l()?;
+ let resources = env
+ .call_method(
+ &context,
+ "getResources",
+ "()Landroid/content/res/Resources;",
+ &[],
+ )?
+ .l()?;
+ let metrics = env
+ .call_method(
+ &resources,
+ "getDisplayMetrics",
+ "()Landroid/util/DisplayMetrics;",
+ &[],
+ )?
+ .l()?;
+ let scale_factor = env.get_field(&metrics, "density", "F")?.f()? as f64;
+
Ok(Self {
+ scale_factor,
double_tap_timeout: env
.call_static_method(CL, "getDoubleTapTimeout", "()I", &[])?
.i()?,