├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── app ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── org │ │ └── linebender │ │ └── android │ │ └── viewdemo │ │ ├── DemoActivity.java │ │ └── DemoView.java │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ └── values │ └── styles.xml ├── build.gradle ├── demo ├── Cargo.toml └── src │ ├── access_ids.rs │ ├── lib.rs │ └── text.rs ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── build.gradle └── src │ └── main │ └── java │ └── org │ └── linebender │ └── android │ └── rustview │ ├── RustInputConnection.java │ └── RustView.java ├── masonry-app ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── org │ │ └── linebender │ │ └── android │ │ └── masonrydemo │ │ ├── DemoActivity.java │ │ └── DemoView.java │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ └── values │ └── styles.xml ├── masonry-demo ├── Cargo.toml └── src │ └── lib.rs ├── masonry ├── Cargo.toml └── src │ ├── app_driver.rs │ └── lib.rs ├── settings.gradle └── src ├── accessibility.rs ├── binder.rs ├── bundle.rs ├── callback_ctx.rs ├── context.rs ├── events.rs ├── graphics.rs ├── ime.rs ├── lib.rs ├── surface.rs ├── util.rs ├── view.rs └── view_configuration.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .gradle 3 | build 4 | jniLibs 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | ".", 4 | "demo", 5 | "masonry", 6 | "masonry-demo", 7 | ] 8 | 9 | [package] 10 | name = "android-view" 11 | version = "0.1.0" 12 | edition = "2024" 13 | 14 | [dependencies] 15 | dpi = { version = "0.1.2", default-features = false } 16 | jni = "0.21.1" 17 | ndk = "0.9.0" 18 | num_enum = "0.7.3" 19 | send_wrapper = "0.6.0" 20 | smallvec = "1.15.0" 21 | ui-events = "0.1.0" 22 | 23 | [profile.dev] 24 | panic = "abort" 25 | 26 | [profile.release] 27 | panic = "abort" 28 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WIP: Implement an Android view in Rust 2 | 3 | This will be a library that can be reused to implement an Android view in Rust. Things that will distinguish it from `android-activity`: 4 | 5 | * No C++ code. 6 | * Meant to be usable for both pure-Rust Android applications and embedding Rust widgets in existing applications. 7 | * Event handlers and other view callbacks run directly on the UI thread; there is no sending events to a separate Rust event loop thread. 8 | * This crate intends to stick as close as possible to the Android framework. This will be especially important for text input support. 9 | 10 | ## Building and running the demos 11 | 12 | ### Common setup 13 | 14 | ```bash 15 | export ANDROID_NDK_HOME="path/to/ndk" 16 | export ANDROID_HOME="path/to/sdk" 17 | 18 | rustup target add aarch64-linux-android 19 | cargo install cargo-ndk 20 | ``` 21 | 22 | ### Simple editor demo 23 | 24 | ```bash 25 | cargo ndk -t arm64-v8a -o app/src/main/jniLibs/ build -p android-view-demo 26 | ./gradlew build 27 | ./gradlew installDebug 28 | adb shell am start -n org.linebender.android.viewdemo/.DemoActivity 29 | # To view logs: 30 | adb shell run-as org.linebender.android.viewdemo logcat -v color 31 | ``` 32 | 33 | ### Masonry demo 34 | 35 | ```bash 36 | cargo ndk -t arm64-v8a -o masonry-app/src/main/jniLibs/ build -p android-view-masonry-demo 37 | ./gradlew build 38 | ./gradlew installDebug 39 | adb shell am start -n org.linebender.android.masonrydemo/.DemoActivity 40 | # To view logs: 41 | adb shell run-as org.linebender.android.masonrydemo logcat -v color 42 | ``` 43 | 44 | ## Open questions 45 | 46 | * 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. 47 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | group = "org.linebender.android.rustview" 6 | 7 | android { 8 | ndkVersion "25.2.9519653" 9 | compileSdk 31 10 | 11 | defaultConfig { 12 | applicationId "org.linebender.android.viewdemo" 13 | minSdk 28 14 | targetSdk 33 15 | versionCode 1 16 | versionName "1.0" 17 | 18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 19 | } 20 | 21 | buildTypes { 22 | release { 23 | minifyEnabled false 24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 25 | } 26 | debug { 27 | minifyEnabled false 28 | //packagingOptions { 29 | // doNotStrip '**/*.so' 30 | //} 31 | // debuggable true 32 | } 33 | } 34 | compileOptions { 35 | sourceCompatibility JavaVersion.VERSION_1_8 36 | targetCompatibility JavaVersion.VERSION_1_8 37 | } 38 | namespace "org.linebender.android.viewdemo" 39 | } 40 | 41 | dependencies { 42 | implementation 'androidx.appcompat:appcompat:1.2.0' 43 | implementation 'androidx.core:core:1.5.0' 44 | implementation project(":library") 45 | } 46 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/org/linebender/android/viewdemo/DemoActivity.java: -------------------------------------------------------------------------------- 1 | package org.linebender.android.viewdemo; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | import android.widget.FrameLayout; 7 | 8 | public final class DemoActivity extends Activity { 9 | static { 10 | System.loadLibrary("main"); 11 | } 12 | 13 | @Override 14 | public void onCreate(Bundle state) { 15 | super.onCreate(state); 16 | View view = new DemoView(this); 17 | view.setLayoutParams( 18 | new FrameLayout.LayoutParams( 19 | FrameLayout.LayoutParams.MATCH_PARENT, 20 | FrameLayout.LayoutParams.MATCH_PARENT)); 21 | view.setFocusable(true); 22 | view.setFocusableInTouchMode(true); 23 | FrameLayout layout = new FrameLayout(this); 24 | layout.addView(view); 25 | setContentView(layout); 26 | view.requestFocus(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/org/linebender/android/viewdemo/DemoView.java: -------------------------------------------------------------------------------- 1 | package org.linebender.android.viewdemo; 2 | 3 | import android.content.Context; 4 | 5 | import org.linebender.android.rustview.RustView; 6 | 7 | public final class DemoView extends RustView { 8 | @Override 9 | protected native long newViewPeer(Context context); 10 | 11 | public DemoView(Context context) { 12 | super(context); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwcampbell/android-view/b1c4f5cde4fd7a833a23f281d572ebd1ff6610ec/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwcampbell/android-view/b1c4f5cde4fd7a833a23f281d572ebd1ff6610ec/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwcampbell/android-view/b1c4f5cde4fd7a833a23f281d572ebd1ff6610ec/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwcampbell/android-view/b1c4f5cde4fd7a833a23f281d572ebd1ff6610ec/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwcampbell/android-view/b1c4f5cde4fd7a833a23f281d572ebd1ff6610ec/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwcampbell/android-view/b1c4f5cde4fd7a833a23f281d572ebd1ff6610ec/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwcampbell/android-view/b1c4f5cde4fd7a833a23f281d572ebd1ff6610ec/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwcampbell/android-view/b1c4f5cde4fd7a833a23f281d572ebd1ff6610ec/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwcampbell/android-view/b1c4f5cde4fd7a833a23f281d572ebd1ff6610ec/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwcampbell/android-view/b1c4f5cde4fd7a833a23f281d572ebd1ff6610ec/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | id 'com.android.application' version '8.9.0' apply false 4 | id 'com.android.library' version '8.9.0' apply false 5 | } 6 | 7 | task clean(type: Delete) { 8 | delete rootProject.buildDir 9 | } 10 | 11 | -------------------------------------------------------------------------------- /demo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "android-view-demo" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [lib] 7 | name = "main" 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | accesskit = "0.19.0" 12 | accesskit_android = "0.2.0" 13 | android-view = { path = ".." } 14 | android_logger = "0.15.0" 15 | anyhow = "1.0.96" 16 | log = "0.4.26" 17 | parley = { git = "https://github.com/linebender/parley", branch = "editor-features-for-android-ime", features = ["accesskit"] } 18 | peniko = { version = "0.4.0", default-features = false } 19 | pollster = "0.4.0" 20 | ui-events = "0.1.0" 21 | vello = "0.5.0" 22 | 23 | # Send tracing events to Android GPU inspector, for profiling 24 | tracing_android_trace = "0.1.1" 25 | tracing-subscriber = "0.3.19" 26 | # Make events recorded with profiling (e.g. in wgpu) visible to Android GPU inspector 27 | profiling = { version = "1.0.16", features = ["profile-with-tracing"] } 28 | # Make events recorded to `tracing` visible in logcat 29 | tracing = { version = "0.1.38", features = ["log-always"] } 30 | -------------------------------------------------------------------------------- /demo/src/access_ids.rs: -------------------------------------------------------------------------------- 1 | // Derived from vello_editor 2 | // Copyright 2024 the Parley Authors 3 | // SPDX-License-Identifier: Apache-2.0 OR MIT 4 | 5 | use accesskit::NodeId; 6 | use core::sync::atomic::{AtomicU64, Ordering}; 7 | 8 | pub const WINDOW_ID: NodeId = NodeId(0); 9 | pub const TEXT_INPUT_ID: NodeId = NodeId(1); 10 | 11 | pub fn next_node_id() -> NodeId { 12 | static NEXT: AtomicU64 = AtomicU64::new(2); 13 | NodeId(NEXT.fetch_add(1, Ordering::Relaxed)) 14 | } 15 | -------------------------------------------------------------------------------- /demo/src/text.rs: -------------------------------------------------------------------------------- 1 | // Derived from vello_editor 2 | // Copyright 2024 the Parley Authors 3 | // SPDX-License-Identifier: Apache-2.0 OR MIT 4 | 5 | use accesskit::{Node, TreeUpdate}; 6 | use core::default::Default; 7 | pub use parley::editor::Generation; 8 | use parley::{ 9 | FontContext, GenericFamily, LayoutContext, PlainEditor, PlainEditorDriver, StyleProperty, 10 | editor::SplitString, layout::PositionedLayoutItem, 11 | }; 12 | use std::time::{Duration, Instant}; 13 | use ui_events::{ 14 | keyboard::{Code, Key, KeyState, KeyboardEvent, NamedKey}, 15 | pointer::{PointerButton, PointerEvent, PointerState, PointerUpdate}, 16 | }; 17 | use vello::{ 18 | Scene, 19 | kurbo::{Affine, Line, Stroke}, 20 | peniko::color::palette, 21 | peniko::{Brush, Fill}, 22 | }; 23 | 24 | use crate::access_ids::next_node_id; 25 | 26 | pub const INSET: f32 = 32.0; 27 | 28 | pub struct Editor { 29 | font_cx: FontContext, 30 | layout_cx: LayoutContext, 31 | editor: PlainEditor, 32 | cursor_visible: bool, 33 | start_time: Option, 34 | blink_period: Duration, 35 | } 36 | 37 | impl Editor { 38 | pub fn new(text: &str) -> Self { 39 | let mut editor = PlainEditor::new(48.0); 40 | editor.set_text(text); 41 | editor.set_scale(1.0); 42 | let styles = editor.edit_styles(); 43 | styles.insert(StyleProperty::LineHeight(1.2)); 44 | styles.insert(GenericFamily::SystemUi.into()); 45 | styles.insert(StyleProperty::Brush(palette::css::WHITE.into())); 46 | let mut result = Self { 47 | font_cx: Default::default(), 48 | layout_cx: Default::default(), 49 | editor, 50 | cursor_visible: Default::default(), 51 | start_time: Default::default(), 52 | blink_period: Default::default(), 53 | }; 54 | result.driver().move_to_text_end(); 55 | result 56 | } 57 | 58 | pub fn driver(&mut self) -> PlainEditorDriver<'_, Brush> { 59 | self.editor.driver(&mut self.font_cx, &mut self.layout_cx) 60 | } 61 | 62 | pub fn editor(&self) -> &PlainEditor { 63 | &self.editor 64 | } 65 | 66 | pub fn editor_mut(&mut self) -> &mut PlainEditor { 67 | &mut self.editor 68 | } 69 | 70 | pub fn text(&self) -> SplitString<'_> { 71 | self.editor.text() 72 | } 73 | 74 | pub fn utf8_to_utf16_index(&self, utf8_index: usize) -> usize { 75 | let mut utf16_len_so_far = 0usize; 76 | let mut utf8_len_so_far = 0usize; 77 | for c in self.editor.raw_text().chars() { 78 | if utf8_len_so_far >= utf8_index { 79 | break; 80 | } 81 | utf16_len_so_far += c.len_utf16(); 82 | utf8_len_so_far += c.len_utf8(); 83 | } 84 | utf16_len_so_far 85 | } 86 | 87 | pub fn utf16_to_utf8_index(&self, utf16_index: usize) -> usize { 88 | let mut utf16_len_so_far = 0usize; 89 | let mut utf8_len_so_far = 0usize; 90 | for c in self.editor.raw_text().chars() { 91 | if utf16_len_so_far >= utf16_index { 92 | break; 93 | } 94 | utf16_len_so_far += c.len_utf16(); 95 | utf8_len_so_far += c.len_utf8(); 96 | } 97 | utf8_len_so_far 98 | } 99 | 100 | pub fn utf8_to_usv_index(&self, utf8_index: usize) -> usize { 101 | let mut usv_len_so_far = 0usize; 102 | let mut utf8_len_so_far = 0usize; 103 | for c in self.editor.raw_text().chars() { 104 | if utf8_len_so_far >= utf8_index { 105 | break; 106 | } 107 | usv_len_so_far += 1; 108 | utf8_len_so_far += c.len_utf8(); 109 | } 110 | usv_len_so_far 111 | } 112 | 113 | pub fn usv_to_utf8_index(&self, usv_index: usize) -> usize { 114 | let mut usv_len_so_far = 0usize; 115 | let mut utf8_len_so_far = 0usize; 116 | for c in self.editor.raw_text().chars() { 117 | if usv_len_so_far >= usv_index { 118 | break; 119 | } 120 | usv_len_so_far += 1; 121 | utf8_len_so_far += c.len_utf8(); 122 | } 123 | utf8_len_so_far 124 | } 125 | 126 | pub fn cursor_reset(&mut self) { 127 | self.start_time = Some(Instant::now()); 128 | // TODO: for real world use, this should be reading from the system settings 129 | self.blink_period = Duration::from_millis(500); 130 | self.cursor_visible = true; 131 | } 132 | 133 | pub fn disable_blink(&mut self) { 134 | self.start_time = None; 135 | } 136 | 137 | pub fn next_blink_time(&self) -> Option { 138 | self.start_time.map(|start_time| { 139 | let phase = Instant::now().duration_since(start_time); 140 | 141 | start_time 142 | + Duration::from_nanos( 143 | ((phase.as_nanos() / self.blink_period.as_nanos() + 1) 144 | * self.blink_period.as_nanos()) as u64, 145 | ) 146 | }) 147 | } 148 | 149 | pub fn cursor_blink(&mut self) { 150 | self.cursor_visible = self.start_time.is_some_and(|start_time| { 151 | let elapsed = Instant::now().duration_since(start_time); 152 | (elapsed.as_millis() / self.blink_period.as_millis()) % 2 == 0 153 | }); 154 | } 155 | 156 | pub fn on_keyboard_event(&mut self, ev: KeyboardEvent) -> bool { 157 | self.cursor_reset(); 158 | let mut drv = self.editor.driver(&mut self.font_cx, &mut self.layout_cx); 159 | 160 | match ev { 161 | // TODO: clipboard commands? 162 | KeyboardEvent { 163 | state: KeyState::Up, 164 | .. 165 | } => { 166 | // All other handlers are for KeyState::Down. 167 | return false; 168 | } 169 | KeyboardEvent { 170 | code: Code::KeyA, 171 | modifiers, 172 | .. 173 | } if modifiers.ctrl() => { 174 | if modifiers.shift() { 175 | drv.collapse_selection(); 176 | } else { 177 | drv.select_all(); 178 | } 179 | } 180 | KeyboardEvent { 181 | key: Key::Named(NamedKey::ArrowLeft), 182 | modifiers, 183 | .. 184 | } => { 185 | if modifiers.ctrl() { 186 | if modifiers.shift() { 187 | drv.select_word_left(); 188 | } else { 189 | drv.move_word_left(); 190 | } 191 | } else if modifiers.shift() { 192 | drv.select_left(); 193 | } else { 194 | drv.move_left(); 195 | } 196 | } 197 | KeyboardEvent { 198 | key: Key::Named(NamedKey::ArrowRight), 199 | modifiers, 200 | .. 201 | } => { 202 | if modifiers.ctrl() { 203 | if modifiers.shift() { 204 | drv.select_word_right(); 205 | } else { 206 | drv.move_word_right(); 207 | } 208 | } else if modifiers.shift() { 209 | drv.select_right(); 210 | } else { 211 | drv.move_right(); 212 | } 213 | } 214 | KeyboardEvent { 215 | key: Key::Named(NamedKey::ArrowUp), 216 | modifiers, 217 | .. 218 | } => { 219 | if modifiers.shift() { 220 | drv.select_up(); 221 | } else { 222 | drv.move_up(); 223 | } 224 | } 225 | KeyboardEvent { 226 | key: Key::Named(NamedKey::ArrowDown), 227 | modifiers, 228 | .. 229 | } => { 230 | if modifiers.shift() { 231 | drv.select_down(); 232 | } else { 233 | drv.move_down(); 234 | } 235 | } 236 | KeyboardEvent { 237 | key: Key::Named(NamedKey::Home), 238 | modifiers, 239 | .. 240 | } => { 241 | if modifiers.ctrl() { 242 | if modifiers.shift() { 243 | drv.select_to_text_start(); 244 | } else { 245 | drv.move_to_text_start(); 246 | } 247 | } else if modifiers.shift() { 248 | drv.select_to_line_start(); 249 | } else { 250 | drv.move_to_line_start(); 251 | } 252 | } 253 | KeyboardEvent { 254 | key: Key::Named(NamedKey::End), 255 | modifiers, 256 | .. 257 | } => { 258 | if modifiers.ctrl() { 259 | if modifiers.shift() { 260 | drv.select_to_text_end(); 261 | } else { 262 | drv.move_to_text_end(); 263 | } 264 | } else if modifiers.shift() { 265 | drv.select_to_line_end(); 266 | } else { 267 | drv.move_to_line_end(); 268 | } 269 | } 270 | KeyboardEvent { 271 | key: Key::Named(NamedKey::Delete), 272 | modifiers, 273 | .. 274 | } => { 275 | if modifiers.ctrl() { 276 | drv.delete_word(); 277 | } else { 278 | drv.delete(); 279 | } 280 | } 281 | KeyboardEvent { 282 | key: Key::Named(NamedKey::Backspace), 283 | modifiers, 284 | .. 285 | } => { 286 | if modifiers.ctrl() { 287 | drv.backdelete_word(); 288 | } else { 289 | drv.backdelete(); 290 | } 291 | } 292 | KeyboardEvent { 293 | key: Key::Named(NamedKey::Enter), 294 | .. 295 | } => { 296 | drv.insert_or_replace_selection("\n"); 297 | } 298 | KeyboardEvent { 299 | key: Key::Character(s), 300 | .. 301 | } => { 302 | drv.insert_or_replace_selection(&s); 303 | } 304 | _ => { 305 | return false; 306 | } 307 | } 308 | true 309 | } 310 | 311 | pub fn handle_pointer_event(&mut self, ev: PointerEvent) -> bool { 312 | let mut drv = self.editor.driver(&mut self.font_cx, &mut self.layout_cx); 313 | match ev { 314 | PointerEvent::Down { 315 | button: None | Some(PointerButton::Primary), 316 | state: 317 | PointerState { 318 | position, 319 | count, 320 | modifiers, 321 | .. 322 | }, 323 | .. 324 | } => match count { 325 | 2 => drv.select_word_at_point(position.x as f32 - INSET, position.y as f32 - INSET), 326 | 3 => drv.select_line_at_point(position.x as f32 - INSET, position.y as f32 - INSET), 327 | 1 if modifiers.shift() => drv.extend_selection_to_point( 328 | position.x as f32 - INSET, 329 | position.y as f32 - INSET, 330 | ), 331 | _ => drv.move_to_point(position.x as f32 - INSET, position.y as f32 - INSET), 332 | }, 333 | PointerEvent::Move(PointerUpdate { 334 | current: PointerState { position, .. }, 335 | .. 336 | }) => { 337 | drv.extend_selection_to_point(position.x as f32 - INSET, position.y as f32 - INSET); 338 | } 339 | PointerEvent::Cancel(..) => { 340 | drv.collapse_selection(); 341 | } 342 | _ => { 343 | return false; 344 | } 345 | } 346 | true 347 | } 348 | 349 | pub fn handle_accesskit_action_request(&mut self, req: &accesskit::ActionRequest) { 350 | if req.action == accesskit::Action::SetTextSelection { 351 | if let Some(accesskit::ActionData::SetTextSelection(selection)) = &req.data { 352 | self.driver().select_from_accesskit(selection); 353 | } 354 | } 355 | } 356 | 357 | /// Return the current `Generation` of the layout. 358 | pub fn generation(&self) -> Generation { 359 | self.editor.generation() 360 | } 361 | 362 | /// Draw into scene. 363 | /// 364 | /// Returns drawn `Generation`. 365 | pub fn draw(&mut self, scene: &mut Scene) -> Generation { 366 | let transform = Affine::translate((INSET as f64, INSET as f64)); 367 | self.editor.selection_geometry_with(|rect, _| { 368 | scene.fill( 369 | Fill::NonZero, 370 | transform, 371 | palette::css::STEEL_BLUE, 372 | None, 373 | &rect, 374 | ); 375 | }); 376 | if self.cursor_visible { 377 | if let Some(cursor) = self.editor.cursor_geometry(5.0) { 378 | scene.fill( 379 | Fill::NonZero, 380 | transform, 381 | palette::css::CADET_BLUE, 382 | None, 383 | &cursor, 384 | ); 385 | } 386 | } 387 | let layout = self.editor.layout(&mut self.font_cx, &mut self.layout_cx); 388 | for line in layout.lines() { 389 | for item in line.items() { 390 | let PositionedLayoutItem::GlyphRun(glyph_run) = item else { 391 | continue; 392 | }; 393 | let style = glyph_run.style(); 394 | // We draw underlines under the text, then the strikethrough on top, following: 395 | // https://drafts.csswg.org/css-text-decor/#painting-order 396 | if let Some(underline) = &style.underline { 397 | let underline_brush = &style.brush; 398 | let run_metrics = glyph_run.run().metrics(); 399 | let offset = match underline.offset { 400 | Some(offset) => offset, 401 | None => run_metrics.underline_offset, 402 | }; 403 | let width = match underline.size { 404 | Some(size) => size, 405 | None => run_metrics.underline_size, 406 | }; 407 | // The `offset` is the distance from the baseline to the top of the underline 408 | // so we move the line down by half the width 409 | // Remember that we are using a y-down coordinate system 410 | // If there's a custom width, because this is an underline, we want the custom 411 | // width to go down from the default expectation 412 | let y = glyph_run.baseline() - offset + width / 2.; 413 | 414 | let line = Line::new( 415 | (glyph_run.offset() as f64, y as f64), 416 | ((glyph_run.offset() + glyph_run.advance()) as f64, y as f64), 417 | ); 418 | scene.stroke( 419 | &Stroke::new(width.into()), 420 | transform, 421 | underline_brush, 422 | None, 423 | &line, 424 | ); 425 | } 426 | let mut x = glyph_run.offset(); 427 | let y = glyph_run.baseline(); 428 | let run = glyph_run.run(); 429 | let font = run.font(); 430 | let font_size = run.font_size(); 431 | let synthesis = run.synthesis(); 432 | let glyph_xform = synthesis 433 | .skew() 434 | .map(|angle| Affine::skew(angle.to_radians().tan() as f64, 0.0)); 435 | scene 436 | .draw_glyphs(font) 437 | .brush(&style.brush) 438 | .hint(true) 439 | .transform(transform) 440 | .glyph_transform(glyph_xform) 441 | .font_size(font_size) 442 | .normalized_coords(run.normalized_coords()) 443 | .draw( 444 | Fill::NonZero, 445 | glyph_run.glyphs().map(|glyph| { 446 | let gx = x + glyph.x; 447 | let gy = y - glyph.y; 448 | x += glyph.advance; 449 | vello::Glyph { 450 | id: glyph.id as _, 451 | x: gx, 452 | y: gy, 453 | } 454 | }), 455 | ); 456 | if let Some(strikethrough) = &style.strikethrough { 457 | let strikethrough_brush = &style.brush; 458 | let run_metrics = glyph_run.run().metrics(); 459 | let offset = match strikethrough.offset { 460 | Some(offset) => offset, 461 | None => run_metrics.strikethrough_offset, 462 | }; 463 | let width = match strikethrough.size { 464 | Some(size) => size, 465 | None => run_metrics.strikethrough_size, 466 | }; 467 | // The `offset` is the distance from the baseline to the *top* of the strikethrough 468 | // so we calculate the middle y-position of the strikethrough based on the font's 469 | // standard strikethrough width. 470 | // Remember that we are using a y-down coordinate system 471 | let y = glyph_run.baseline() - offset + run_metrics.strikethrough_size / 2.; 472 | 473 | let line = Line::new( 474 | (glyph_run.offset() as f64, y as f64), 475 | ((glyph_run.offset() + glyph_run.advance()) as f64, y as f64), 476 | ); 477 | scene.stroke( 478 | &Stroke::new(width.into()), 479 | transform, 480 | strikethrough_brush, 481 | None, 482 | &line, 483 | ); 484 | } 485 | } 486 | } 487 | self.editor.generation() 488 | } 489 | 490 | pub fn accessibility(&mut self, update: &mut TreeUpdate, node: &mut Node) { 491 | let mut drv = self.editor.driver(&mut self.font_cx, &mut self.layout_cx); 492 | drv.accessibility(update, node, next_node_id, INSET.into(), INSET.into()); 493 | } 494 | } 495 | 496 | pub const LOREM: &str = r" Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi cursus mi sed euismod euismod. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam placerat efficitur tellus at semper. Morbi ac risus magna. Donec ut cursus ex. Etiam quis posuere tellus. Mauris posuere dui et turpis mollis, vitae luctus tellus consectetur. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur eu facilisis nisl. 497 | 498 | Phasellus in viverra dolor, vitae facilisis est. Maecenas malesuada massa vel ultricies feugiat. Vivamus venenatis et gהתעשייה בנושא האינטרנטa nibh nec pharetra. Phasellus vestibulum elit enim, nec scelerisque orci faucibus id. Vivamus consequat purus sit amet orci egestas, non iaculis massa porttitor. Vestibulum ut eros leo. In fermentum convallis magna in finibus. Donec justo leo, maximus ac laoreet id, volutpat ut elit. Mauris sed leo non neque laoreet faucibus. Aliquam orci arcu, faucibus in molestie eget, ornare non dui. Donec volutpat nulla in fringilla elementum. Aliquam vitae ante egestas ligula tempus vestibulum sit amet sed ante. "; 499 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Enables namespacing of each library's R class so that its R class includes only the 19 | # resources declared in the library itself and none from the library's dependencies, 20 | # thereby reducing the size of the R class for that library 21 | android.nonTransitiveRClass=true 22 | android.defaults.buildfeatures.buildconfig=true 23 | android.nonFinalResIds=false -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwcampbell/android-view/b1c4f5cde4fd7a833a23f281d572ebd1ff6610ec/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon May 02 15:39:12 BST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | } 4 | 5 | group = "org.linebender.android.rustview" 6 | 7 | android { 8 | compileSdk 31 9 | defaultConfig { 10 | minSdk 28 11 | versionCode 1 12 | versionName "1.0.0" 13 | } 14 | namespace 'org.linebender.android.rustview' 15 | } 16 | 17 | dependencies { 18 | implementation 'androidx.appcompat:appcompat:1.2.0' 19 | implementation 'androidx.core:core:1.5.0' 20 | } 21 | -------------------------------------------------------------------------------- /library/src/main/java/org/linebender/android/rustview/RustInputConnection.java: -------------------------------------------------------------------------------- 1 | package org.linebender.android.rustview; 2 | 3 | import android.os.Bundle; 4 | import android.os.Handler; 5 | import android.view.KeyEvent; 6 | import android.view.inputmethod.CompletionInfo; 7 | import android.view.inputmethod.CorrectionInfo; 8 | import android.view.inputmethod.ExtractedText; 9 | import android.view.inputmethod.ExtractedTextRequest; 10 | import android.view.inputmethod.InputConnection; 11 | import android.view.inputmethod.InputContentInfo; 12 | 13 | class RustInputConnection implements InputConnection { 14 | private final RustView mView; 15 | 16 | RustInputConnection(RustView view) { 17 | mView = view; 18 | } 19 | 20 | private long getViewPeer() { 21 | return mView.mViewPeer; 22 | } 23 | 24 | @Override 25 | public CharSequence getTextBeforeCursor(int n, int flags) { 26 | return mView.getTextBeforeCursorNative(getViewPeer(), n); 27 | } 28 | 29 | @Override 30 | public CharSequence getTextAfterCursor(int n, int flags) { 31 | return mView.getTextAfterCursorNative(getViewPeer(), n); 32 | } 33 | 34 | @Override 35 | public CharSequence getSelectedText(int flags) { 36 | return mView.getSelectedTextNative(getViewPeer()); 37 | } 38 | 39 | @Override 40 | public int getCursorCapsMode(int reqModes) { 41 | return mView.getCursorCapsModeNative(getViewPeer(), reqModes); 42 | } 43 | 44 | @Override 45 | public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { 46 | return null; 47 | } 48 | 49 | @Override 50 | public boolean deleteSurroundingText(int beforeLength, int afterLength) { 51 | return mView.deleteSurroundingTextNative(getViewPeer(), beforeLength, afterLength); 52 | } 53 | 54 | @Override 55 | public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) { 56 | return mView.deleteSurroundingTextInCodePointsNative(getViewPeer(), beforeLength, afterLength); 57 | } 58 | 59 | @Override 60 | public boolean setComposingText(CharSequence text, int newCursorPosition) { 61 | return mView.setComposingTextNative(getViewPeer(), text.toString(), newCursorPosition); 62 | } 63 | 64 | @Override 65 | public boolean setComposingRegion(int start, int end) { 66 | return mView.setComposingRegionNative(getViewPeer(), start, end); 67 | } 68 | 69 | @Override 70 | public boolean finishComposingText() { 71 | return mView.finishComposingTextNative(getViewPeer()); 72 | } 73 | 74 | @Override 75 | public boolean commitText(CharSequence text, int newCursorPosition) { 76 | return mView.commitTextNative(getViewPeer(), text.toString(), newCursorPosition); 77 | } 78 | 79 | @Override 80 | public boolean commitCompletion(CompletionInfo text) { 81 | return false; 82 | } 83 | 84 | @Override 85 | public boolean commitCorrection(CorrectionInfo correctionInfo) { 86 | return false; 87 | } 88 | 89 | @Override 90 | public boolean setSelection(int start, int end) { 91 | return mView.setSelectionNative(getViewPeer(), start, end); 92 | } 93 | 94 | @Override 95 | public boolean performEditorAction(int editorAction) { 96 | return mView.performEditorActionNative(getViewPeer(), editorAction); 97 | } 98 | 99 | @Override 100 | public boolean performContextMenuAction(int id) { 101 | return mView.performContextMenuActionNative(getViewPeer(), id); 102 | } 103 | 104 | @Override 105 | public boolean beginBatchEdit() { 106 | return mView.beginBatchEditNative(getViewPeer()); 107 | } 108 | 109 | @Override 110 | public boolean endBatchEdit() { 111 | return mView.endBatchEditNative(getViewPeer()); 112 | } 113 | 114 | @Override 115 | public boolean sendKeyEvent(KeyEvent event) { 116 | return mView.inputConnectionSendKeyEventNative(getViewPeer(), event); 117 | } 118 | 119 | @Override 120 | public boolean clearMetaKeyStates(int states) { 121 | return mView.inputConnectionClearMetaKeyStatesNative(getViewPeer(), states); 122 | } 123 | 124 | @Override 125 | public boolean reportFullscreenMode(boolean enabled) { 126 | return mView.inputConnectionReportFullscreenModeNative(getViewPeer(), enabled); 127 | } 128 | 129 | @Override 130 | public boolean performPrivateCommand(String action, Bundle data) { 131 | return false; 132 | } 133 | 134 | @Override 135 | public boolean requestCursorUpdates(int cursorUpdateMode) { 136 | return mView.requestCursorUpdatesNative(getViewPeer(), cursorUpdateMode); 137 | } 138 | 139 | @Override 140 | public Handler getHandler() { 141 | return null; 142 | } 143 | 144 | @Override 145 | public void closeConnection() { 146 | mView.closeInputConnectionNative(getViewPeer()); 147 | } 148 | 149 | @Override 150 | public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) { 151 | return false; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /library/src/main/java/org/linebender/android/rustview/RustView.java: -------------------------------------------------------------------------------- 1 | package org.linebender.android.rustview; 2 | 3 | import android.content.Context; 4 | import android.graphics.Rect; 5 | import android.os.Bundle; 6 | import android.view.Choreographer; 7 | import android.view.KeyEvent; 8 | import android.view.MotionEvent; 9 | import android.view.SurfaceHolder; 10 | import android.view.SurfaceView; 11 | import android.view.accessibility.AccessibilityNodeInfo; 12 | import android.view.accessibility.AccessibilityNodeProvider; 13 | import android.view.inputmethod.EditorInfo; 14 | import android.view.inputmethod.InputConnection; 15 | import android.view.inputmethod.InputMethodManager; 16 | 17 | public abstract class RustView extends SurfaceView 18 | implements SurfaceHolder.Callback, Choreographer.FrameCallback { 19 | final long mViewPeer; 20 | final InputMethodManager mInputMethodManager; 21 | 22 | protected abstract long newViewPeer(Context context); 23 | 24 | public RustView(Context context) { 25 | super(context); 26 | mViewPeer = newViewPeer(context); 27 | getHolder().addCallback(this); 28 | mInputMethodManager = 29 | (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); 30 | } 31 | 32 | private native int[] onMeasureNative(long peer, int widthSpec, int heightSpec); 33 | 34 | @Override 35 | protected void onMeasure(int widthSpec, int heightSpec) { 36 | int[] result = onMeasureNative(mViewPeer, widthSpec, heightSpec); 37 | if (result != null) { 38 | setMeasuredDimension(result[0], result[1]); 39 | } else { 40 | super.onMeasure(widthSpec, heightSpec); 41 | } 42 | } 43 | 44 | private native void onLayoutNative( 45 | long peer, boolean changed, int left, int top, int right, int bottom); 46 | 47 | @Override 48 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 49 | onLayoutNative(mViewPeer, changed, left, top, right, bottom); 50 | super.onLayout(changed, left, top, right, bottom); 51 | } 52 | 53 | private native void onSizeChangedNative(long peer, int w, int h, int oldw, int oldh); 54 | 55 | @Override 56 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 57 | onSizeChangedNative(mViewPeer, w, h, oldw, oldh); 58 | super.onSizeChanged(w, h, oldw, oldh); 59 | } 60 | 61 | private native boolean onKeyDownNative(long peer, int keyCode, KeyEvent event); 62 | 63 | @Override 64 | public boolean onKeyDown(int keyCode, KeyEvent event) { 65 | return onKeyDownNative(mViewPeer, keyCode, event) || super.onKeyDown(keyCode, event); 66 | } 67 | 68 | private native boolean onKeyUpNative(long peer, int keyCode, KeyEvent event); 69 | 70 | @Override 71 | public boolean onKeyUp(int keyCode, KeyEvent event) { 72 | return onKeyUpNative(mViewPeer, keyCode, event) || super.onKeyUp(keyCode, event); 73 | } 74 | 75 | private native boolean onTrackballEventNative(long peer, MotionEvent event); 76 | 77 | @Override 78 | public boolean onTrackballEvent(MotionEvent event) { 79 | return onTrackballEventNative(mViewPeer, event) || super.onTrackballEvent(event); 80 | } 81 | 82 | private native boolean onTouchEventNative(long peer, MotionEvent event); 83 | 84 | @Override 85 | public boolean onTouchEvent(MotionEvent event) { 86 | return onTouchEventNative(mViewPeer, event) || super.onTouchEvent(event); 87 | } 88 | 89 | private native boolean onGenericMotionEventNative(long peer, MotionEvent event); 90 | 91 | @Override 92 | public boolean onGenericMotionEvent(MotionEvent event) { 93 | return onGenericMotionEventNative(mViewPeer, event) || super.onGenericMotionEvent(event); 94 | } 95 | 96 | private native boolean onHoverEventNative(long peer, MotionEvent event); 97 | 98 | @Override 99 | public boolean onHoverEvent(MotionEvent event) { 100 | return onHoverEventNative(mViewPeer, event) || super.onHoverEvent(event); 101 | } 102 | 103 | private native void onFocusChangedNative( 104 | long peer, boolean gainFocus, int direction, Rect previouslyFocusedRect); 105 | 106 | @Override 107 | protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 108 | super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 109 | onFocusChangedNative(mViewPeer, gainFocus, direction, previouslyFocusedRect); 110 | } 111 | 112 | private native void onWindowFocusChangedNative(long peer, boolean hasWindowFocus); 113 | 114 | @Override 115 | public void onWindowFocusChanged(boolean hasWindowFocus) { 116 | super.onWindowFocusChanged(hasWindowFocus); 117 | onWindowFocusChangedNative(mViewPeer, hasWindowFocus); 118 | } 119 | 120 | private native void onAttachedToWindowNative(long peer); 121 | 122 | @Override 123 | protected void onAttachedToWindow() { 124 | super.onAttachedToWindow(); 125 | onAttachedToWindowNative(mViewPeer); 126 | } 127 | 128 | private native void onDetachedFromWindowNative(long peer); 129 | 130 | @Override 131 | protected void onDetachedFromWindow() { 132 | super.onDetachedFromWindow(); 133 | onDetachedFromWindowNative(mViewPeer); 134 | } 135 | 136 | private native void onWindowVisibilityChangedNative(long peer, int visibility); 137 | 138 | @Override 139 | protected void onWindowVisibilityChanged(int visibility) { 140 | super.onWindowVisibilityChanged(visibility); 141 | onWindowVisibilityChangedNative(mViewPeer, visibility); 142 | } 143 | 144 | private native void surfaceCreatedNative(long peer, SurfaceHolder holder); 145 | 146 | @Override 147 | public void surfaceCreated(SurfaceHolder holder) { 148 | surfaceCreatedNative(mViewPeer, holder); 149 | } 150 | 151 | private native void surfaceChangedNative( 152 | long peer, SurfaceHolder holder, int format, int width, int height); 153 | 154 | @Override 155 | public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 156 | surfaceChangedNative(mViewPeer, holder, format, width, height); 157 | } 158 | 159 | private native void surfaceDestroyedNative(long peer, SurfaceHolder holder); 160 | 161 | @Override 162 | public void surfaceDestroyed(SurfaceHolder holder) { 163 | surfaceDestroyedNative(mViewPeer, holder); 164 | } 165 | 166 | void postFrameCallback() { 167 | Choreographer c = Choreographer.getInstance(); 168 | c.removeFrameCallback(this); 169 | c.postFrameCallback(this); 170 | } 171 | 172 | void removeFrameCallback() { 173 | Choreographer.getInstance().removeFrameCallback(this); 174 | } 175 | 176 | private native void doFrameNative(long peer, long frameTimeNanos); 177 | 178 | @Override 179 | public void doFrame(long frameTimeNanos) { 180 | doFrameNative(mViewPeer, frameTimeNanos); 181 | } 182 | 183 | private native void delayedCallbackNative(long peer); 184 | 185 | private final Runnable mDelayedCallback = 186 | new Runnable() { 187 | @Override 188 | public void run() { 189 | delayedCallbackNative(mViewPeer); 190 | } 191 | }; 192 | 193 | boolean postDelayed(long delayMillis) { 194 | return postDelayed(mDelayedCallback, delayMillis); 195 | } 196 | 197 | boolean removeDelayedCallbacks() { 198 | return removeCallbacks(mDelayedCallback); 199 | } 200 | 201 | private native boolean hasAccessibilityNodeProviderNative(long peer); 202 | 203 | private native AccessibilityNodeInfo createAccessibilityNodeInfoNative( 204 | long peer, int virtualViewId); 205 | 206 | private native AccessibilityNodeInfo accessibilityFindFocusNative(long peer, int virtualViewId); 207 | 208 | private native boolean performAccessibilityActionNative( 209 | long peer, int virtualViewId, int action, Bundle arguments); 210 | 211 | @Override 212 | public AccessibilityNodeProvider getAccessibilityNodeProvider() { 213 | if (!hasAccessibilityNodeProviderNative(mViewPeer)) { 214 | return super.getAccessibilityNodeProvider(); 215 | } 216 | return new AccessibilityNodeProvider() { 217 | @Override 218 | public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { 219 | return createAccessibilityNodeInfoNative(mViewPeer, virtualViewId); 220 | } 221 | 222 | @Override 223 | public AccessibilityNodeInfo findFocus(int focusType) { 224 | return accessibilityFindFocusNative(mViewPeer, focusType); 225 | } 226 | 227 | @Override 228 | public boolean performAction(int virtualViewId, int action, Bundle arguments) { 229 | return performAccessibilityActionNative( 230 | mViewPeer, virtualViewId, action, arguments); 231 | } 232 | }; 233 | } 234 | 235 | private native boolean onCreateInputConnectionNative(long peer, EditorInfo outAttrs); 236 | 237 | @Override 238 | public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 239 | if (!onCreateInputConnectionNative(mViewPeer, outAttrs)) { 240 | return null; 241 | } 242 | return new RustInputConnection(this); 243 | } 244 | 245 | native String getTextBeforeCursorNative(long peer, int n); 246 | 247 | native String getTextAfterCursorNative(long peer, int n); 248 | 249 | native String getSelectedTextNative(long peer); 250 | 251 | native int getCursorCapsModeNative(long peer, int reqModes); 252 | 253 | native boolean deleteSurroundingTextNative(long peer, int beforeLength, int afterLength); 254 | 255 | native boolean deleteSurroundingTextInCodePointsNative( 256 | long peer, int beforeLength, int afterLength); 257 | 258 | native boolean setComposingTextNative(long peer, String text, int newCursorPosition); 259 | 260 | native boolean setComposingRegionNative(long peer, int start, int end); 261 | 262 | native boolean finishComposingTextNative(long peer); 263 | 264 | native boolean commitTextNative(long peer, String text, int newCursorPosition); 265 | 266 | native boolean setSelectionNative(long peer, int start, int end); 267 | 268 | native boolean performEditorActionNative(long peer, int editorAction); 269 | 270 | native boolean performContextMenuActionNative(long peer, int id); 271 | 272 | native boolean beginBatchEditNative(long peer); 273 | 274 | native boolean endBatchEditNative(long peer); 275 | 276 | native boolean inputConnectionSendKeyEventNative(long peer, KeyEvent event); 277 | 278 | native boolean inputConnectionClearMetaKeyStatesNative(long peer, int states); 279 | 280 | native boolean inputConnectionReportFullscreenModeNative(long peer, boolean enabled); 281 | 282 | native boolean requestCursorUpdatesNative(long peer, int cursorUpdateMode); 283 | 284 | native void closeInputConnectionNative(long peer); 285 | } 286 | -------------------------------------------------------------------------------- /masonry-app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | group = "org.linebender.android.rustview" 6 | 7 | android { 8 | ndkVersion "25.2.9519653" 9 | compileSdk 31 10 | 11 | defaultConfig { 12 | applicationId "org.linebender.android.masonrydemo" 13 | minSdk 28 14 | targetSdk 33 15 | versionCode 1 16 | versionName "1.0" 17 | 18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 19 | } 20 | 21 | buildTypes { 22 | release { 23 | minifyEnabled false 24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 25 | } 26 | debug { 27 | minifyEnabled false 28 | //packagingOptions { 29 | // doNotStrip '**/*.so' 30 | //} 31 | // debuggable true 32 | } 33 | } 34 | compileOptions { 35 | sourceCompatibility JavaVersion.VERSION_1_8 36 | targetCompatibility JavaVersion.VERSION_1_8 37 | } 38 | namespace "org.linebender.android.masonrydemo" 39 | } 40 | 41 | dependencies { 42 | implementation 'androidx.appcompat:appcompat:1.2.0' 43 | implementation 'androidx.core:core:1.5.0' 44 | implementation project(":library") 45 | } 46 | -------------------------------------------------------------------------------- /masonry-app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /masonry-app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /masonry-app/src/main/java/org/linebender/android/masonrydemo/DemoActivity.java: -------------------------------------------------------------------------------- 1 | package org.linebender.android.masonrydemo; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | import android.widget.FrameLayout; 7 | 8 | public final class DemoActivity extends Activity { 9 | static { 10 | System.loadLibrary("main"); 11 | } 12 | 13 | @Override 14 | public void onCreate(Bundle state) { 15 | super.onCreate(state); 16 | View view = new DemoView(this); 17 | view.setLayoutParams( 18 | new FrameLayout.LayoutParams( 19 | FrameLayout.LayoutParams.MATCH_PARENT, 20 | FrameLayout.LayoutParams.MATCH_PARENT)); 21 | view.setFocusable(true); 22 | view.setFocusableInTouchMode(true); 23 | FrameLayout layout = new FrameLayout(this); 24 | layout.addView(view); 25 | setContentView(layout); 26 | view.requestFocus(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /masonry-app/src/main/java/org/linebender/android/masonrydemo/DemoView.java: -------------------------------------------------------------------------------- 1 | package org.linebender.android.masonrydemo; 2 | 3 | import android.content.Context; 4 | 5 | import org.linebender.android.rustview.RustView; 6 | 7 | public final class DemoView extends RustView { 8 | @Override 9 | protected native long newViewPeer(Context context); 10 | 11 | public DemoView(Context context) { 12 | super(context); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /masonry-app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /masonry-app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /masonry-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /masonry-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /masonry-app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwcampbell/android-view/b1c4f5cde4fd7a833a23f281d572ebd1ff6610ec/masonry-app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /masonry-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwcampbell/android-view/b1c4f5cde4fd7a833a23f281d572ebd1ff6610ec/masonry-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /masonry-app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwcampbell/android-view/b1c4f5cde4fd7a833a23f281d572ebd1ff6610ec/masonry-app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /masonry-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwcampbell/android-view/b1c4f5cde4fd7a833a23f281d572ebd1ff6610ec/masonry-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /masonry-app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwcampbell/android-view/b1c4f5cde4fd7a833a23f281d572ebd1ff6610ec/masonry-app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /masonry-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwcampbell/android-view/b1c4f5cde4fd7a833a23f281d572ebd1ff6610ec/masonry-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /masonry-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwcampbell/android-view/b1c4f5cde4fd7a833a23f281d572ebd1ff6610ec/masonry-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /masonry-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwcampbell/android-view/b1c4f5cde4fd7a833a23f281d572ebd1ff6610ec/masonry-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /masonry-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwcampbell/android-view/b1c4f5cde4fd7a833a23f281d572ebd1ff6610ec/masonry-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /masonry-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwcampbell/android-view/b1c4f5cde4fd7a833a23f281d572ebd1ff6610ec/masonry-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /masonry-app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /masonry-demo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "android-view-masonry-demo" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [lib] 7 | name = "main" 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | android-view = { path = ".." } 12 | masonry = { git = "https://github.com/linebender/xilem" } 13 | masonry_android = { path = "../masonry" } 14 | 15 | # Send tracing events to Android GPU inspector, for profiling 16 | tracing_android_trace = "0.1.1" 17 | tracing-subscriber = "0.3.19" 18 | # Make events recorded with profiling (e.g. in wgpu) visible to Android GPU inspector 19 | profiling = { version = "1.0.16", features = ["profile-with-tracing"] } 20 | # Make events recorded to `tracing` visible in logcat 21 | tracing = { version = "0.1.38", features = ["log-always"] } 22 | -------------------------------------------------------------------------------- /masonry-demo/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Xilem Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | #![deny(unsafe_op_in_unsafe_fn)] 5 | 6 | use android_view::{ 7 | jni::{ 8 | JNIEnv, JavaVM, 9 | sys::{JNI_VERSION_1_6, JavaVM as RawJavaVM, jint, jlong}, 10 | }, 11 | *, 12 | }; 13 | use masonry::{ 14 | core::{Action, Widget, WidgetId}, 15 | peniko::Color, 16 | widgets::{Button, Flex, Label, Portal, RootWidget, TextArea, Textbox}, 17 | }; 18 | use masonry_android::{AppDriver, DriverCtx}; 19 | use std::ffi::c_void; 20 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 21 | 22 | const VERTICAL_WIDGET_SPACING: f64 = 20.0; 23 | 24 | struct Driver { 25 | next_task: String, 26 | } 27 | 28 | impl AppDriver for Driver { 29 | fn on_action(&mut self, ctx: &mut DriverCtx<'_>, _widget_id: WidgetId, action: Action) { 30 | match action { 31 | Action::ButtonPressed(_) => { 32 | ctx.render_root().edit_root_widget(|mut root| { 33 | let mut root = root.downcast::(); 34 | 35 | let mut portal = RootWidget::child_mut(&mut root); 36 | let mut portal = portal.downcast::>(); 37 | let mut flex = Portal::child_mut(&mut portal); 38 | Flex::add_child(&mut flex, Label::new(self.next_task.clone())); 39 | 40 | let mut first_row = Flex::child_mut(&mut flex, 0).unwrap(); 41 | let mut first_row = first_row.downcast::(); 42 | let mut textbox = Flex::child_mut(&mut first_row, 0).unwrap(); 43 | let mut textbox = textbox.downcast::(); 44 | let mut text_area = Textbox::text_mut(&mut textbox); 45 | TextArea::reset_text(&mut text_area, ""); 46 | }); 47 | } 48 | Action::TextChanged(new_text) => { 49 | self.next_task = new_text.clone(); 50 | } 51 | _ => {} 52 | } 53 | } 54 | } 55 | 56 | fn make_widget_tree() -> impl Widget { 57 | Portal::new( 58 | Flex::column() 59 | .with_child( 60 | Flex::row() 61 | .with_flex_child(Textbox::new(""), 1.0) 62 | .with_child(Button::new("Add task")), 63 | ) 64 | .with_spacer(VERTICAL_WIDGET_SPACING), 65 | ) 66 | } 67 | 68 | extern "system" fn new_view_peer<'local>( 69 | mut env: JNIEnv<'local>, 70 | _view: View<'local>, 71 | context: Context<'local>, 72 | ) -> jlong { 73 | masonry_android::new_view_peer( 74 | &mut env, 75 | &context, 76 | RootWidget::new(make_widget_tree()), 77 | Driver { 78 | next_task: String::new(), 79 | }, 80 | Color::BLACK, 81 | ) 82 | } 83 | 84 | /// Symbol run at JNI load time. 85 | /// 86 | /// # Safety 87 | /// There is no alternative, interacting with JNI is always unsafe at some level. 88 | #[unsafe(no_mangle)] 89 | pub unsafe extern "system" fn JNI_OnLoad(vm: *mut RawJavaVM, _: *mut c_void) -> jint { 90 | // This will try to create a "log" logger, and error because one was already created above 91 | // We therefore ignore the error 92 | // Ideally, we'd only ignore the SetLoggerError, but the only way that's possible is to inspect 93 | // `Debug/Display` on the TryInitError, which is awful. 94 | let _ = tracing_subscriber::registry() 95 | .with(tracing_android_trace::AndroidTraceLayer::new()) 96 | .try_init(); 97 | 98 | let vm = unsafe { JavaVM::from_raw(vm) }.unwrap(); 99 | let mut env = vm.get_env().unwrap(); 100 | register_view_class( 101 | &mut env, 102 | "org/linebender/android/masonrydemo/DemoView", 103 | new_view_peer, 104 | ); 105 | JNI_VERSION_1_6 106 | } 107 | -------------------------------------------------------------------------------- /masonry/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "masonry_android" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | accesskit = "0.19.0" 8 | accesskit_android = "0.2.0" 9 | android-view = { path = ".." } 10 | masonry = { git = "https://github.com/linebender/xilem" } 11 | pollster = "0.4.0" 12 | tracing = "0.1.40" 13 | vello = "0.5.0" 14 | -------------------------------------------------------------------------------- /masonry/src/app_driver.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 the Xilem Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use masonry::{ 5 | app::RenderRoot, 6 | core::{Action, WidgetId}, 7 | }; 8 | 9 | use crate::MasonryState; 10 | 11 | /// Context for the [`AppDriver`] trait. 12 | /// 13 | /// Currently holds a reference to the [`RenderRoot`]. 14 | pub struct DriverCtx<'a> { 15 | // We make no guarantees about the fields of this struct, but 16 | // they must all be public so that the type can be constructed 17 | // externally. 18 | // This is needed for external users, whilst our external API 19 | // is not yet designed. 20 | #[doc(hidden)] 21 | pub render_root: &'a mut RenderRoot, 22 | } 23 | 24 | /// A trait for defining how your app interacts with the Masonry widget tree. 25 | /// 26 | /// When launching your app with [`crate::app::run`], you need to provide 27 | /// a type that implements this trait. 28 | pub trait AppDriver { 29 | /// A hook which will be executed when a widget emits an [`Action`]. 30 | fn on_action(&mut self, ctx: &mut DriverCtx<'_>, widget_id: WidgetId, action: Action); 31 | 32 | #[expect(unused_variables, reason = "Default impl doesn't use arguments")] 33 | /// A hook which will be executed when the application starts, to allow initial configuration of the `MasonryState`. 34 | /// 35 | /// Use cases include loading fonts. 36 | fn on_start(&mut self, state: &mut MasonryState) {} 37 | } 38 | 39 | impl DriverCtx<'_> { 40 | // TODO - Add method to create timer 41 | 42 | /// Access the [`RenderRoot`]. 43 | pub fn render_root(&mut self) -> &mut RenderRoot { 44 | self.render_root 45 | } 46 | 47 | /// Returns `true` if something happened that requires a rewrite pass or a re-render. 48 | pub fn content_changed(&self) -> bool { 49 | self.render_root.needs_rewrite_passes() 50 | } 51 | } 52 | 53 | #[cfg(doctest)] 54 | /// Doctests aren't collected under `cfg(test)`; we can use `cfg(doctest)` instead 55 | mod doctests { 56 | /// ```no_run 57 | /// use masonry::app::DriverCtx; 58 | /// let _ctx = DriverCtx { 59 | /// render_root: unimplemented!() 60 | /// }; 61 | /// ``` 62 | const _DRIVER_CTX_EXTERNALLY_CONSTRUCTIBLE: () = {}; 63 | } 64 | -------------------------------------------------------------------------------- /masonry/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Xilem Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use accesskit::{ActionHandler, ActionRequest, ActivationHandler, TreeUpdate}; 5 | use android_view::{ 6 | jni::{ 7 | JNIEnv, 8 | sys::{jint, jlong}, 9 | }, 10 | ndk::{event::Keycode, native_window::NativeWindow}, 11 | *, 12 | }; 13 | use masonry::{ 14 | Handled, 15 | app::{RenderRoot, RenderRootOptions, RenderRootSignal, WindowSizePolicy}, 16 | core::{TextEvent, Widget, WindowEvent}, 17 | dpi::PhysicalSize, 18 | peniko::Color, 19 | }; 20 | use tracing::{debug, info, info_span}; 21 | use vello::{ 22 | Renderer, RendererOptions, Scene, 23 | kurbo::Affine, 24 | util::{RenderContext, RenderSurface}, 25 | wgpu::{ 26 | self, PresentMode, 27 | rwh::{DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, WindowHandle}, 28 | }, 29 | }; 30 | 31 | mod app_driver; 32 | pub use app_driver::*; 33 | 34 | // From VelloCompose 35 | struct AndroidWindowHandle { 36 | window: NativeWindow, 37 | } 38 | 39 | impl HasDisplayHandle for AndroidWindowHandle { 40 | fn display_handle(&self) -> Result, HandleError> { 41 | Ok(DisplayHandle::android()) 42 | } 43 | } 44 | 45 | impl HasWindowHandle for AndroidWindowHandle { 46 | fn window_handle(&self) -> Result, HandleError> { 47 | self.window.window_handle() 48 | } 49 | } 50 | 51 | /// Helper function that creates a vello `Renderer` for a given `RenderContext` and `RenderSurface` 52 | fn create_vello_renderer(render_cx: &RenderContext, surface: &RenderSurface<'_>) -> Renderer { 53 | Renderer::new( 54 | &render_cx.devices[surface.dev_id].device, 55 | RendererOptions { 56 | use_cpu: false, 57 | antialiasing_support: vello::AaSupport::area_only(), 58 | num_init_threads: None, 59 | // TODO: add pipeline cache. 60 | pipeline_cache: None, 61 | }, 62 | ) 63 | .expect("Couldn't create renderer") 64 | } 65 | 66 | fn scale_factor<'local>(env: &mut JNIEnv<'local>, android_ctx: &Context<'local>) -> f64 { 67 | let res = android_ctx.resources(env); 68 | let metrics = res.display_metrics(env); 69 | metrics.density(env) as f64 70 | } 71 | 72 | fn show_soft_input<'local>(env: &mut JNIEnv<'local>, view: &View<'local>) { 73 | let imm = view.input_method_manager(env); 74 | imm.show_soft_input(env, view, 0); 75 | } 76 | 77 | fn hide_soft_input<'local>(env: &mut JNIEnv<'local>, view: &View<'local>) { 78 | let imm = view.input_method_manager(env); 79 | let window_token = view.window_token(env); 80 | imm.hide_soft_input_from_window(env, &window_token, 0); 81 | } 82 | 83 | pub struct MasonryState { 84 | render_cx: RenderContext, 85 | render_root: RenderRoot, 86 | tap_counter: TapCounter, 87 | renderer: Option, 88 | render_surface: Option>, 89 | accesskit_adapter: accesskit_android::Adapter, 90 | background_color: Color, 91 | } 92 | 93 | impl MasonryState { 94 | pub fn new(root_widget: impl Widget, background_color: Color, scale_factor: f64) -> Self { 95 | let render_cx = RenderContext::new(); 96 | 97 | Self { 98 | render_cx, 99 | render_root: RenderRoot::new( 100 | root_widget, 101 | RenderRootOptions { 102 | use_system_fonts: true, 103 | size_policy: WindowSizePolicy::User, 104 | scale_factor, 105 | test_font: None, 106 | }, 107 | ), 108 | renderer: None, 109 | tap_counter: TapCounter::default(), 110 | render_surface: None, 111 | accesskit_adapter: Default::default(), 112 | background_color, 113 | } 114 | } 115 | } 116 | 117 | #[derive(Default)] 118 | struct MasonryAccessActivationHandler { 119 | requested_initial_tree: bool, 120 | } 121 | 122 | impl ActivationHandler for MasonryAccessActivationHandler { 123 | fn request_initial_tree(&mut self) -> Option { 124 | self.requested_initial_tree = true; 125 | None 126 | } 127 | } 128 | 129 | struct MasonryAccessActionHandler<'a> { 130 | render_root: &'a mut RenderRoot, 131 | } 132 | 133 | impl ActionHandler for MasonryAccessActionHandler<'_> { 134 | fn do_action(&mut self, request: ActionRequest) { 135 | self.render_root.handle_access_event(request); 136 | } 137 | } 138 | 139 | struct MasonryViewPeer { 140 | state: MasonryState, 141 | app_driver: Driver, 142 | } 143 | 144 | impl MasonryViewPeer { 145 | fn handle_signals(&mut self, ctx: &mut CallbackCtx) { 146 | let mut needs_redraw = false; 147 | while let Some(signal) = self.state.render_root.pop_signal() { 148 | match signal { 149 | RenderRootSignal::Action(action, widget_id) => { 150 | let mut driver_ctx = DriverCtx { 151 | render_root: &mut self.state.render_root, 152 | }; 153 | debug!("Action {:?} on widget {:?}", action, widget_id); 154 | self.app_driver 155 | .on_action(&mut driver_ctx, widget_id, action); 156 | } 157 | RenderRootSignal::StartIme => { 158 | ctx.push_static_deferred_callback(show_soft_input); 159 | } 160 | RenderRootSignal::EndIme => { 161 | ctx.push_static_deferred_callback(hide_soft_input); 162 | } 163 | RenderRootSignal::ImeMoved(_position, _size) => { 164 | // TODO 165 | } 166 | RenderRootSignal::RequestRedraw => { 167 | needs_redraw = true; 168 | } 169 | RenderRootSignal::RequestAnimFrame => { 170 | // Does this need to do something different from RequestRedraw? 171 | needs_redraw = true; 172 | } 173 | RenderRootSignal::TakeFocus => { 174 | // TODO 175 | } 176 | RenderRootSignal::SetCursor(_cursor) => { 177 | // TODO? 178 | } 179 | RenderRootSignal::SetSize(_size) => { 180 | // TODO: Does this ever apply, maybe for embedded views? 181 | } 182 | RenderRootSignal::SetTitle(_title) => { 183 | // TODO: Does this ever apply? 184 | } 185 | RenderRootSignal::DragWindow => { 186 | // TODO: Does this ever apply? 187 | } 188 | RenderRootSignal::DragResizeWindow(_direction) => { 189 | // TODO: Does this ever apply? 190 | } 191 | RenderRootSignal::ToggleMaximized => { 192 | // TODO: Does this ever apply? 193 | } 194 | RenderRootSignal::Minimize => { 195 | // TODO: Does this ever apply? 196 | } 197 | RenderRootSignal::Exit => { 198 | // TODO: Should we do something with this? 199 | } 200 | RenderRootSignal::ShowWindowMenu(_position) => { 201 | // TODO: Does this ever apply? 202 | } 203 | RenderRootSignal::WidgetSelectedInInspector(widget_id) => { 204 | let Some(widget) = self.state.render_root.get_widget(widget_id) else { 205 | return; 206 | }; 207 | let widget_name = widget.short_type_name(); 208 | let display_name = if let Some(debug_text) = widget.get_debug_text() { 209 | format!("{widget_name}<{debug_text}>") 210 | } else { 211 | widget_name.into() 212 | }; 213 | info!("Widget selected in inspector: {widget_id} - {display_name}"); 214 | } 215 | } 216 | } 217 | 218 | // If we're processing a lot of actions, we may have a lot of pending redraws. 219 | // We batch them up to avoid redundant requests. 220 | if needs_redraw && self.state.render_surface.is_some() { 221 | ctx.view.post_frame_callback(&mut ctx.env); 222 | } 223 | } 224 | 225 | fn redraw(&mut self, ctx: &mut CallbackCtx) { 226 | let _span = info_span!("redraw"); 227 | self.state 228 | .render_root 229 | .handle_window_event(WindowEvent::AnimFrame); 230 | let (scene, tree_update) = self.state.render_root.redraw(); 231 | 232 | if let Some(events) = self 233 | .state 234 | .accesskit_adapter 235 | .update_if_active(|| tree_update) 236 | { 237 | ctx.push_dynamic_deferred_callback(move |env, view| { 238 | events.raise(env, &view.0); 239 | }); 240 | } 241 | 242 | let android_ctx = ctx.view.context(&mut ctx.env); 243 | let scale_factor = scale_factor(&mut ctx.env, &android_ctx); 244 | let scene = if scale_factor == 1.0 { 245 | scene 246 | } else { 247 | let mut new_scene = Scene::new(); 248 | new_scene.append(&scene, Some(Affine::scale(scale_factor))); 249 | new_scene 250 | }; 251 | 252 | // Get the RenderSurface (surface + config). 253 | let surface = self.state.render_surface.as_ref().unwrap(); 254 | 255 | // Get the window size. 256 | let width = surface.config.width; 257 | let height = surface.config.height; 258 | 259 | // Get a handle to the device. 260 | let device_handle = &self.state.render_cx.devices[surface.dev_id]; 261 | 262 | // Render to the surface's texture. 263 | self.state 264 | .renderer 265 | .as_mut() 266 | .unwrap() 267 | .render_to_texture( 268 | &device_handle.device, 269 | &device_handle.queue, 270 | &scene, 271 | &surface.target_view, 272 | &vello::RenderParams { 273 | base_color: self.state.background_color, 274 | width, 275 | height, 276 | antialiasing_method: vello::AaConfig::Area, 277 | }, 278 | ) 279 | .expect("failed to render to surface"); 280 | 281 | // Get the surface's texture. 282 | let surface_texture = surface 283 | .surface 284 | .get_current_texture() 285 | .expect("failed to get surface texture"); 286 | 287 | // Perform the copy. 288 | let mut encoder = 289 | device_handle 290 | .device 291 | .create_command_encoder(&wgpu::CommandEncoderDescriptor { 292 | label: Some("Surface Blit"), 293 | }); 294 | surface.blitter.copy( 295 | &device_handle.device, 296 | &mut encoder, 297 | &surface.target_view, 298 | &surface_texture 299 | .texture 300 | .create_view(&wgpu::TextureViewDescriptor::default()), 301 | ); 302 | device_handle.queue.submit([encoder.finish()]); 303 | // Queue the texture to be presented on the surface. 304 | surface_texture.present(); 305 | 306 | device_handle.device.poll(wgpu::Maintain::Poll); 307 | } 308 | 309 | fn on_key_event<'local>( 310 | &mut self, 311 | ctx: &mut CallbackCtx<'local>, 312 | event: &KeyEvent<'local>, 313 | ) -> bool { 314 | let handled = self 315 | .state 316 | .render_root 317 | .handle_text_event(TextEvent::Keyboard(event.to_keyboard_event(&mut ctx.env))); 318 | self.handle_signals(ctx); 319 | matches!(handled, Handled::Yes) 320 | } 321 | 322 | fn with_access_activation_handler<'local, T>( 323 | &mut self, 324 | ctx: &mut CallbackCtx<'local>, 325 | f: impl FnOnce( 326 | &mut CallbackCtx<'local>, 327 | &mut accesskit_android::Adapter, 328 | &mut MasonryAccessActivationHandler, 329 | ) -> T, 330 | ) -> T { 331 | let mut handler = MasonryAccessActivationHandler::default(); 332 | let result = f(ctx, &mut self.state.accesskit_adapter, &mut handler); 333 | if handler.requested_initial_tree { 334 | self.state 335 | .render_root 336 | .handle_window_event(WindowEvent::RebuildAccessTree); 337 | self.handle_signals(ctx); 338 | } 339 | result 340 | } 341 | } 342 | 343 | impl ViewPeer for MasonryViewPeer { 344 | fn on_key_down<'local>( 345 | &mut self, 346 | ctx: &mut CallbackCtx<'local>, 347 | _: Keycode, 348 | event: &KeyEvent<'local>, 349 | ) -> bool { 350 | self.on_key_event(ctx, event) 351 | } 352 | 353 | fn on_key_up<'local>( 354 | &mut self, 355 | ctx: &mut CallbackCtx<'local>, 356 | _: Keycode, 357 | event: &KeyEvent<'local>, 358 | ) -> bool { 359 | self.on_key_event(ctx, event) 360 | } 361 | 362 | fn on_touch_event<'local>( 363 | &mut self, 364 | ctx: &mut CallbackCtx<'local>, 365 | event: &MotionEvent<'local>, 366 | ) -> bool { 367 | let Some(ev) = event.to_pointer_event(&mut ctx.env, &self.state.tap_counter.vc) else { 368 | return false; 369 | }; 370 | let ev = self.state.tap_counter.attach_count(ev); 371 | self.state.render_root.handle_pointer_event(ev); 372 | self.handle_signals(ctx); 373 | true 374 | } 375 | 376 | fn on_generic_motion_event<'local>( 377 | &mut self, 378 | ctx: &mut CallbackCtx<'local>, 379 | event: &MotionEvent<'local>, 380 | ) -> bool { 381 | self.on_touch_event(ctx, event) 382 | } 383 | 384 | fn on_hover_event<'local>( 385 | &mut self, 386 | ctx: &mut CallbackCtx<'local>, 387 | event: &MotionEvent<'local>, 388 | ) -> bool { 389 | let action = event.action(&mut ctx.env); 390 | let x = event.x(&mut ctx.env); 391 | let y = event.y(&mut ctx.env); 392 | if let Some(events) = self.with_access_activation_handler(ctx, |_ctx, adapter, handler| { 393 | adapter.on_hover_event(handler, action, x, y) 394 | }) { 395 | ctx.push_dynamic_deferred_callback(move |env, view| { 396 | events.raise(env, &view.0); 397 | }); 398 | true 399 | } else { 400 | self.on_touch_event(ctx, event) 401 | } 402 | } 403 | 404 | fn on_focus_changed<'local>( 405 | &mut self, 406 | ctx: &mut CallbackCtx<'local>, 407 | gain_focus: bool, 408 | _direction: jint, 409 | _previously_focused_rect: Option<&Rect<'local>>, 410 | ) { 411 | self.state 412 | .render_root 413 | .handle_text_event(TextEvent::WindowFocusChange(gain_focus)); 414 | self.handle_signals(ctx); 415 | } 416 | 417 | fn surface_changed<'local>( 418 | &mut self, 419 | ctx: &mut CallbackCtx<'local>, 420 | holder: &SurfaceHolder<'local>, 421 | _format: jint, 422 | width: jint, 423 | height: jint, 424 | ) { 425 | self.state.tap_counter = TapCounter::new(ctx.view.view_configuration(&mut ctx.env)); 426 | let android_ctx = ctx.view.context(&mut ctx.env); 427 | let scale_factor = scale_factor(&mut ctx.env, &android_ctx); 428 | self.state 429 | .render_root 430 | .handle_window_event(WindowEvent::Rescale(scale_factor)); 431 | let size = PhysicalSize { 432 | width: width as u32, 433 | height: height as u32, 434 | }; 435 | self.state 436 | .render_root 437 | .handle_window_event(WindowEvent::Resize(size)); 438 | 439 | let window = holder.surface(&mut ctx.env).to_native_window(&mut ctx.env); 440 | // Drop the old surface, if any, that owned the native window 441 | // before creating a new one. Otherwise, we crash with 442 | // ERROR_NATIVE_WINDOW_IN_USE_KHR. 443 | self.state.render_surface = None; 444 | let surface = self 445 | .state 446 | .render_cx 447 | .instance 448 | .create_surface(wgpu::SurfaceTarget::from(AndroidWindowHandle { window })) 449 | .expect("Error creating surface"); 450 | let dev_id = pollster::block_on(self.state.render_cx.device(Some(&surface))) 451 | .expect("No compatible device"); 452 | let device_handle = &self.state.render_cx.devices[dev_id]; 453 | let capabilities = surface.get_capabilities(device_handle.adapter()); 454 | let present_mode = if capabilities.present_modes.contains(&PresentMode::Mailbox) { 455 | PresentMode::Mailbox 456 | } else { 457 | PresentMode::AutoVsync 458 | }; 459 | 460 | let surface_future = self.state.render_cx.create_render_surface( 461 | surface, 462 | width as _, 463 | height as _, 464 | present_mode, 465 | ); 466 | let surface = pollster::block_on(surface_future).expect("Error creating surface"); 467 | 468 | // Create a vello Renderer for the surface (using its device id) 469 | self.state 470 | .renderer 471 | .get_or_insert_with(|| create_vello_renderer(&self.state.render_cx, &surface)); 472 | self.state.render_surface = Some(surface); 473 | 474 | self.redraw(ctx); 475 | } 476 | 477 | fn surface_destroyed<'local>( 478 | &mut self, 479 | ctx: &mut CallbackCtx<'local>, 480 | _holder: &SurfaceHolder<'local>, 481 | ) { 482 | self.state.render_surface = None; 483 | ctx.view.remove_frame_callback(&mut ctx.env); 484 | } 485 | 486 | fn do_frame(&mut self, ctx: &mut CallbackCtx, _frame_time_nanos: jlong) { 487 | self.redraw(ctx); 488 | } 489 | 490 | fn as_accessibility_node_provider(&mut self) -> Option<&mut dyn AccessibilityNodeProvider> { 491 | Some(self) 492 | } 493 | 494 | fn as_input_connection(&mut self) -> Option<&mut dyn InputConnection> { 495 | // TODO 496 | None 497 | } 498 | } 499 | 500 | impl AccessibilityNodeProvider for MasonryViewPeer { 501 | fn create_accessibility_node_info<'local>( 502 | &mut self, 503 | ctx: &mut CallbackCtx<'local>, 504 | virtual_view_id: jint, 505 | ) -> AccessibilityNodeInfo<'local> { 506 | self.with_access_activation_handler(ctx, |ctx, adapter, handler| { 507 | AccessibilityNodeInfo(adapter.create_accessibility_node_info( 508 | handler, 509 | &mut ctx.env, 510 | &ctx.view.0, 511 | virtual_view_id, 512 | )) 513 | }) 514 | } 515 | 516 | fn find_focus<'local>( 517 | &mut self, 518 | ctx: &mut CallbackCtx<'local>, 519 | focus_type: jint, 520 | ) -> AccessibilityNodeInfo<'local> { 521 | self.with_access_activation_handler(ctx, |ctx, adapter, handler| { 522 | AccessibilityNodeInfo(adapter.find_focus( 523 | handler, 524 | &mut ctx.env, 525 | &ctx.view.0, 526 | focus_type, 527 | )) 528 | }) 529 | } 530 | 531 | fn perform_action<'local>( 532 | &mut self, 533 | ctx: &mut CallbackCtx<'local>, 534 | virtual_view_id: jint, 535 | action: jint, 536 | arguments: &Bundle<'local>, 537 | ) -> bool { 538 | let Some(action) = 539 | accesskit_android::PlatformAction::from_java(&mut ctx.env, action, &arguments.0) 540 | else { 541 | return false; 542 | }; 543 | let mut action_handler = MasonryAccessActionHandler { 544 | render_root: &mut self.state.render_root, 545 | }; 546 | if let Some(events) = self.state.accesskit_adapter.perform_action( 547 | &mut action_handler, 548 | virtual_view_id, 549 | &action, 550 | ) { 551 | ctx.push_dynamic_deferred_callback(move |env, view| { 552 | events.raise(env, &view.0); 553 | }); 554 | self.handle_signals(ctx); 555 | true 556 | } else { 557 | false 558 | } 559 | } 560 | } 561 | 562 | // TODO: InputConnection 563 | 564 | pub fn new_view_peer<'local>( 565 | env: &mut JNIEnv<'local>, 566 | android_ctx: &Context<'local>, 567 | root_widget: impl Widget, 568 | mut app_driver: impl AppDriver + 'static, 569 | background_color: Color, 570 | ) -> jlong { 571 | let scale_factor = scale_factor(env, android_ctx); 572 | let mut state = MasonryState::new(root_widget, background_color, scale_factor); 573 | app_driver.on_start(&mut state); 574 | register_view_peer(MasonryViewPeer { state, app_driver }) 575 | } 576 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | 16 | include ':app' 17 | include ':library' 18 | include ':masonry-app' 19 | -------------------------------------------------------------------------------- /src/accessibility.rs: -------------------------------------------------------------------------------- 1 | use jni::{ 2 | JNIEnv, 3 | objects::JObject, 4 | sys::{jboolean, jint, jlong}, 5 | }; 6 | 7 | use crate::{bundle::*, callback_ctx::*, util::*, view::*}; 8 | 9 | #[derive(Default)] 10 | #[repr(transparent)] 11 | pub struct AccessibilityNodeInfo<'local>(pub JObject<'local>); 12 | 13 | #[allow(unused_variables)] 14 | pub trait AccessibilityNodeProvider { 15 | fn create_accessibility_node_info<'local>( 16 | &mut self, 17 | ctx: &mut CallbackCtx<'local>, 18 | virtual_view_id: jint, 19 | ) -> AccessibilityNodeInfo<'local>; 20 | 21 | fn find_focus<'local>( 22 | &mut self, 23 | ctx: &mut CallbackCtx<'local>, 24 | focus_type: jint, 25 | ) -> AccessibilityNodeInfo<'local>; 26 | 27 | fn perform_action<'local>( 28 | &mut self, 29 | ctx: &mut CallbackCtx<'local>, 30 | virtual_view_id: jint, 31 | action: jint, 32 | arguments: &Bundle<'local>, 33 | ) -> bool; 34 | } 35 | 36 | fn with_accessibility_node_provider<'local, F, T: Default>( 37 | env: JNIEnv<'local>, 38 | view: View<'local>, 39 | id: jlong, 40 | f: F, 41 | ) -> T 42 | where 43 | F: FnOnce(&mut CallbackCtx<'local>, &mut dyn AccessibilityNodeProvider) -> T, 44 | { 45 | with_peer(env, view, id, |ctx, peer| { 46 | let Some(anp) = peer.as_accessibility_node_provider() else { 47 | return T::default(); 48 | }; 49 | f(ctx, anp) 50 | }) 51 | } 52 | 53 | pub(crate) extern "system" fn has_accessibility_node_provider<'local>( 54 | env: JNIEnv<'local>, 55 | view: View<'local>, 56 | peer: jlong, 57 | ) -> jboolean { 58 | as_jboolean(with_accessibility_node_provider( 59 | env, 60 | view, 61 | peer, 62 | |_ctx, _anp| true, 63 | )) 64 | } 65 | 66 | pub(crate) extern "system" fn create_accessibility_node_info<'local>( 67 | env: JNIEnv<'local>, 68 | view: View<'local>, 69 | peer: jlong, 70 | virtual_view_id: jint, 71 | ) -> AccessibilityNodeInfo<'local> { 72 | with_accessibility_node_provider(env, view, peer, |ctx, anp| { 73 | anp.create_accessibility_node_info(ctx, virtual_view_id) 74 | }) 75 | } 76 | 77 | pub(crate) extern "system" fn accessibility_find_focus<'local>( 78 | env: JNIEnv<'local>, 79 | view: View<'local>, 80 | peer: jlong, 81 | focus_type: jint, 82 | ) -> AccessibilityNodeInfo<'local> { 83 | with_accessibility_node_provider(env, view, peer, |ctx, anp| anp.find_focus(ctx, focus_type)) 84 | } 85 | 86 | pub(crate) extern "system" fn perform_accessibility_action<'local>( 87 | env: JNIEnv<'local>, 88 | view: View<'local>, 89 | peer: jlong, 90 | virtual_view_id: jint, 91 | action: jint, 92 | arguments: Bundle<'local>, 93 | ) -> jboolean { 94 | as_jboolean(with_accessibility_node_provider( 95 | env, 96 | view, 97 | peer, 98 | |ctx, anp| anp.perform_action(ctx, virtual_view_id, action, &arguments), 99 | )) 100 | } 101 | -------------------------------------------------------------------------------- /src/binder.rs: -------------------------------------------------------------------------------- 1 | use jni::objects::JObject; 2 | 3 | #[repr(transparent)] 4 | pub struct IBinder<'local>(pub JObject<'local>); 5 | -------------------------------------------------------------------------------- /src/bundle.rs: -------------------------------------------------------------------------------- 1 | use jni::objects::JObject; 2 | 3 | #[repr(transparent)] 4 | pub struct Bundle<'local>(pub JObject<'local>); 5 | -------------------------------------------------------------------------------- /src/callback_ctx.rs: -------------------------------------------------------------------------------- 1 | use jni::JNIEnv; 2 | use smallvec::SmallVec; 3 | 4 | use crate::view::View; 5 | 6 | enum DeferredCallback<'local> { 7 | Static(fn(&mut JNIEnv<'local>, &View<'local>)), 8 | Dynamic(Box, &View<'local>)>), 9 | } 10 | 11 | pub struct CallbackCtx<'local> { 12 | pub env: JNIEnv<'local>, 13 | pub view: View<'local>, 14 | deferred_callbacks: SmallVec<[DeferredCallback<'local>; 4]>, 15 | } 16 | 17 | impl<'local> CallbackCtx<'local> { 18 | pub(crate) fn new(env: JNIEnv<'local>, view: View<'local>) -> Self { 19 | Self { 20 | env, 21 | view, 22 | deferred_callbacks: SmallVec::new(), 23 | } 24 | } 25 | 26 | pub fn push_static_deferred_callback( 27 | &mut self, 28 | callback: fn(&mut JNIEnv<'local>, &View<'local>), 29 | ) { 30 | self.deferred_callbacks 31 | .push(DeferredCallback::Static(callback)); 32 | } 33 | 34 | pub fn push_dynamic_deferred_callback( 35 | &mut self, 36 | callback: impl 'static + FnOnce(&mut JNIEnv<'local>, &View<'local>), 37 | ) { 38 | self.deferred_callbacks 39 | .push(DeferredCallback::Dynamic(Box::new(callback))); 40 | } 41 | } 42 | 43 | impl CallbackCtx<'_> { 44 | pub(crate) fn finish(mut self) { 45 | for callback in self.deferred_callbacks { 46 | match callback { 47 | DeferredCallback::Static(f) => f(&mut self.env, &self.view), 48 | DeferredCallback::Dynamic(f) => f(&mut self.env, &self.view), 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/context.rs: -------------------------------------------------------------------------------- 1 | use jni::{JNIEnv, objects::JObject, sys::jfloat}; 2 | 3 | #[repr(transparent)] 4 | pub struct Context<'local>(pub JObject<'local>); 5 | 6 | impl<'local> Context<'local> { 7 | pub fn resources(&self, env: &mut JNIEnv<'local>) -> Resources<'local> { 8 | Resources( 9 | env.call_method( 10 | &self.0, 11 | "getResources", 12 | "()Landroid/content/res/Resources;", 13 | &[], 14 | ) 15 | .unwrap() 16 | .l() 17 | .unwrap(), 18 | ) 19 | } 20 | 21 | // TODO: more methods? 22 | } 23 | 24 | #[repr(transparent)] 25 | pub struct Resources<'local>(pub JObject<'local>); 26 | 27 | impl<'local> Resources<'local> { 28 | pub fn display_metrics(&self, env: &mut JNIEnv<'local>) -> DisplayMetrics<'local> { 29 | DisplayMetrics( 30 | env.call_method( 31 | &self.0, 32 | "getDisplayMetrics", 33 | "()Landroid/util/DisplayMetrics;", 34 | &[], 35 | ) 36 | .unwrap() 37 | .l() 38 | .unwrap(), 39 | ) 40 | } 41 | } 42 | 43 | #[repr(transparent)] 44 | pub struct DisplayMetrics<'local>(pub JObject<'local>); 45 | 46 | impl<'local> DisplayMetrics<'local> { 47 | pub fn density(&self, env: &mut JNIEnv<'local>) -> jfloat { 48 | env.get_field(&self.0, "density", "F").unwrap().f().unwrap() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/events.rs: -------------------------------------------------------------------------------- 1 | use dpi::PhysicalPosition; 2 | use jni::{ 3 | JNIEnv, 4 | objects::JObject, 5 | sys::{jfloat, jint, jlong}, 6 | }; 7 | use ndk::event::{ 8 | Axis, ButtonState, KeyAction, KeyEventFlags, Keycode, MetaState, MotionAction, 9 | MotionEventFlags, Source, ToolType, 10 | }; 11 | use num_enum::FromPrimitive; 12 | use ui_events::{ 13 | ScrollDelta, 14 | keyboard::{KeyboardEvent, Modifiers}, 15 | pointer::{ContactGeometry, PointerEvent, PointerId, PointerState, PointerUpdate}, 16 | }; 17 | 18 | use crate::ViewConfiguration; 19 | 20 | #[repr(transparent)] 21 | pub struct KeyEvent<'local>(pub JObject<'local>); 22 | 23 | impl<'local> KeyEvent<'local> { 24 | pub fn device_id(&self, env: &mut JNIEnv<'local>) -> jint { 25 | env.call_method(&self.0, "getDeviceId", "()I", &[]) 26 | .unwrap() 27 | .i() 28 | .unwrap() 29 | } 30 | 31 | pub fn source(&self, env: &mut JNIEnv<'local>) -> Source { 32 | Source::from_primitive( 33 | env.call_method(&self.0, "getSource", "()I", &[]) 34 | .unwrap() 35 | .i() 36 | .unwrap(), 37 | ) 38 | } 39 | 40 | pub fn action(&self, env: &mut JNIEnv<'local>) -> KeyAction { 41 | KeyAction::from_primitive( 42 | env.call_method(&self.0, "getAction", "()I", &[]) 43 | .unwrap() 44 | .i() 45 | .unwrap(), 46 | ) 47 | } 48 | 49 | pub fn event_time(&self, env: &mut JNIEnv<'local>) -> jlong { 50 | env.call_method(&self.0, "getEventTime", "()J", &[]) 51 | .unwrap() 52 | .j() 53 | .unwrap() 54 | } 55 | 56 | pub fn down_time(&self, env: &mut JNIEnv<'local>) -> jlong { 57 | env.call_method(&self.0, "getDownTime", "()J", &[]) 58 | .unwrap() 59 | .j() 60 | .unwrap() 61 | } 62 | 63 | pub fn flags(&self, env: &mut JNIEnv<'local>) -> KeyEventFlags { 64 | KeyEventFlags( 65 | env.call_method(&self.0, "getFlags", "()I", &[]) 66 | .unwrap() 67 | .i() 68 | .unwrap() as u32, 69 | ) 70 | } 71 | 72 | pub fn meta_state(&self, env: &mut JNIEnv<'local>) -> MetaState { 73 | MetaState( 74 | env.call_method(&self.0, "getMetaState", "()I", &[]) 75 | .unwrap() 76 | .i() 77 | .unwrap() as u32, 78 | ) 79 | } 80 | 81 | pub fn repeat_count(&self, env: &mut JNIEnv<'local>) -> jint { 82 | env.call_method(&self.0, "getRepeatCount", "()I", &[]) 83 | .unwrap() 84 | .i() 85 | .unwrap() 86 | } 87 | 88 | pub fn key_code(&self, env: &mut JNIEnv<'local>) -> Keycode { 89 | Keycode::from_primitive( 90 | env.call_method(&self.0, "getKeyCode", "()I", &[]) 91 | .unwrap() 92 | .i() 93 | .unwrap(), 94 | ) 95 | } 96 | 97 | pub fn scan_code(&self, env: &mut JNIEnv<'local>) -> jint { 98 | env.call_method(&self.0, "getScanCode", "()I", &[]) 99 | .unwrap() 100 | .i() 101 | .unwrap() 102 | } 103 | 104 | pub fn unicode_char(&self, env: &mut JNIEnv<'local>) -> Option { 105 | let i = env 106 | .call_method(&self.0, "getUnicodeChar", "()I", &[]) 107 | .unwrap() 108 | .i() 109 | .unwrap(); 110 | if i <= 0 { 111 | return None; 112 | } 113 | char::from_u32(i as _) 114 | } 115 | 116 | pub fn to_keyboard_event(&self, env: &mut JNIEnv<'local>) -> KeyboardEvent { 117 | use ui_events::keyboard::{Key, KeyState, NamedKey, android}; 118 | 119 | let key_code = self.key_code(env); 120 | 121 | KeyboardEvent { 122 | state: if self.action(env) == KeyAction::Down { 123 | KeyState::Down 124 | } else { 125 | KeyState::Up 126 | }, 127 | key: match android::keycode_to_named_key(key_code.into()) { 128 | NamedKey::Unidentified => { 129 | if let Some(c) = self.unicode_char(env) { 130 | Key::Character(c.to_string()) 131 | } else { 132 | Key::Named(NamedKey::Unidentified) 133 | } 134 | } 135 | nk => Key::Named(nk), 136 | }, 137 | code: android::keycode_to_code(key_code.into()), 138 | location: android::keycode_to_location(key_code.into()), 139 | modifiers: meta_state_to_modifiers(self.meta_state(env)), 140 | repeat: self.repeat_count(env) != 0, 141 | is_composing: false, 142 | } 143 | } 144 | } 145 | 146 | #[repr(transparent)] 147 | pub struct MotionEvent<'local>(pub JObject<'local>); 148 | 149 | impl<'local> MotionEvent<'local> { 150 | pub fn device_id(&self, env: &mut JNIEnv<'local>) -> jint { 151 | env.call_method(&self.0, "getDeviceId", "()I", &[]) 152 | .unwrap() 153 | .i() 154 | .unwrap() 155 | } 156 | 157 | pub fn source(&self, env: &mut JNIEnv<'local>) -> Source { 158 | Source::from_primitive( 159 | env.call_method(&self.0, "getSource", "()I", &[]) 160 | .unwrap() 161 | .i() 162 | .unwrap(), 163 | ) 164 | } 165 | 166 | pub fn action(&self, env: &mut JNIEnv<'local>) -> jint { 167 | env.call_method(&self.0, "getAction", "()I", &[]) 168 | .unwrap() 169 | .i() 170 | .unwrap() 171 | } 172 | 173 | pub fn action_button(&self, env: &mut JNIEnv<'local>) -> jint { 174 | env.call_method(&self.0, "getActionButton", "()I", &[]) 175 | .unwrap() 176 | .i() 177 | .unwrap() 178 | } 179 | 180 | pub fn action_masked(&self, env: &mut JNIEnv<'local>) -> MotionAction { 181 | MotionAction::from_primitive( 182 | env.call_method(&self.0, "getActionMasked", "()I", &[]) 183 | .unwrap() 184 | .i() 185 | .unwrap(), 186 | ) 187 | } 188 | 189 | pub fn action_index(&self, env: &mut JNIEnv<'local>) -> jint { 190 | env.call_method(&self.0, "getActionIndex", "()I", &[]) 191 | .unwrap() 192 | .i() 193 | .unwrap() 194 | } 195 | 196 | pub fn button_state(&self, env: &mut JNIEnv<'local>) -> ButtonState { 197 | ButtonState( 198 | env.call_method(&self.0, "getButtonState", "()I", &[]) 199 | .unwrap() 200 | .i() 201 | .unwrap() as u32, 202 | ) 203 | } 204 | 205 | pub fn event_time(&self, env: &mut JNIEnv<'local>) -> jlong { 206 | env.call_method(&self.0, "getEventTime", "()J", &[]) 207 | .unwrap() 208 | .j() 209 | .unwrap() 210 | } 211 | 212 | pub fn event_time_nanos(&self, env: &mut JNIEnv<'local>) -> jlong { 213 | env.call_method(&self.0, "getEventTimeNanos", "()J", &[]) 214 | .unwrap() 215 | .j() 216 | .unwrap() 217 | } 218 | 219 | pub fn historical_event_time_nanos(&self, env: &mut JNIEnv<'local>, pos: i32) -> jlong { 220 | env.call_method( 221 | &self.0, 222 | "getHistoricalEventTimeNanos", 223 | "(I)J", 224 | &[pos.into()], 225 | ) 226 | .unwrap() 227 | .j() 228 | .unwrap() 229 | } 230 | 231 | pub fn down_time(&self, env: &mut JNIEnv<'local>) -> jlong { 232 | env.call_method(&self.0, "getDownTime", "()J", &[]) 233 | .unwrap() 234 | .j() 235 | .unwrap() 236 | } 237 | 238 | pub fn flags(&self, env: &mut JNIEnv<'local>) -> MotionEventFlags { 239 | MotionEventFlags( 240 | env.call_method(&self.0, "getFlags", "()I", &[]) 241 | .unwrap() 242 | .i() 243 | .unwrap() as u32, 244 | ) 245 | } 246 | 247 | pub fn meta_state(&self, env: &mut JNIEnv<'local>) -> MetaState { 248 | MetaState( 249 | env.call_method(&self.0, "getMetaState", "()I", &[]) 250 | .unwrap() 251 | .i() 252 | .unwrap() as u32, 253 | ) 254 | } 255 | 256 | pub fn pointer_count(&self, env: &mut JNIEnv<'local>) -> jint { 257 | env.call_method(&self.0, "getPointerCount", "()I", &[]) 258 | .unwrap() 259 | .i() 260 | .unwrap() 261 | } 262 | 263 | pub fn pointer_id(&self, env: &mut JNIEnv<'local>, pointer_index: jint) -> jint { 264 | env.call_method(&self.0, "getPointerId", "(I)I", &[pointer_index.into()]) 265 | .unwrap() 266 | .i() 267 | .unwrap() 268 | } 269 | 270 | pub fn tool_type(&self, env: &mut JNIEnv<'local>, pointer_index: jint) -> ToolType { 271 | ToolType::from( 272 | env.call_method(&self.0, "getToolType", "(I)I", &[pointer_index.into()]) 273 | .unwrap() 274 | .i() 275 | .unwrap(), 276 | ) 277 | } 278 | 279 | pub fn x(&self, env: &mut JNIEnv<'local>) -> jfloat { 280 | env.call_method(&self.0, "getX", "()F", &[]) 281 | .unwrap() 282 | .f() 283 | .unwrap() 284 | } 285 | 286 | pub fn x_at(&self, env: &mut JNIEnv<'local>, pointer_index: jint) -> jfloat { 287 | env.call_method(&self.0, "getX", "(I)F", &[pointer_index.into()]) 288 | .unwrap() 289 | .f() 290 | .unwrap() 291 | } 292 | 293 | pub fn y(&self, env: &mut JNIEnv<'local>) -> jfloat { 294 | env.call_method(&self.0, "getY", "()F", &[]) 295 | .unwrap() 296 | .f() 297 | .unwrap() 298 | } 299 | 300 | pub fn y_at(&self, env: &mut JNIEnv<'local>, pointer_index: jint) -> jfloat { 301 | env.call_method(&self.0, "getY", "(I)F", &[pointer_index.into()]) 302 | .unwrap() 303 | .f() 304 | .unwrap() 305 | } 306 | 307 | pub fn pressure(&self, env: &mut JNIEnv<'local>) -> jfloat { 308 | env.call_method(&self.0, "getPressure", "()F", &[]) 309 | .unwrap() 310 | .f() 311 | .unwrap() 312 | } 313 | 314 | pub fn history_size(&self, env: &mut JNIEnv<'local>) -> jint { 315 | env.call_method(&self.0, "getHistorySize", "()I", &[]) 316 | .unwrap() 317 | .i() 318 | .unwrap() 319 | } 320 | 321 | pub fn historical_axis( 322 | &self, 323 | env: &mut JNIEnv<'local>, 324 | axis: Axis, 325 | pointer_index: i32, 326 | pos: i32, 327 | ) -> jfloat { 328 | env.call_method( 329 | &self.0, 330 | "getHistoricalAxisValue", 331 | "(III)F", 332 | &[i32::from(axis).into(), pointer_index.into(), pos.into()], 333 | ) 334 | .unwrap() 335 | .f() 336 | .unwrap() 337 | } 338 | 339 | pub fn axis(&self, env: &mut JNIEnv<'local>, axis: Axis, pointer_index: jint) -> jfloat { 340 | env.call_method( 341 | &self.0, 342 | "getAxisValue", 343 | "(II)F", 344 | &[i32::from(axis).into(), pointer_index.into()], 345 | ) 346 | .unwrap() 347 | .f() 348 | .unwrap() 349 | } 350 | 351 | pub fn to_pointer_event( 352 | &self, 353 | env: &mut JNIEnv<'local>, 354 | vc: &ViewConfiguration, 355 | ) -> Option { 356 | use ui_events::pointer::{ 357 | PersistentDeviceId, PointerButton, PointerButtons, PointerId, PointerInfo, 358 | PointerOrientation, PointerState, PointerType, PointerUpdate, 359 | }; 360 | 361 | let time = self.event_time_nanos(env) as u64; 362 | let action = self.action_masked(env); 363 | 364 | let action_index = self.action_index(env); 365 | let tool_type = self.tool_type(env, action_index); 366 | if tool_type == ToolType::Palm { 367 | // I don't think we have any useful way of handling this. 368 | return None; 369 | } 370 | let pointer = PointerInfo { 371 | pointer_id: match self.pointer_id(env, action_index) { 372 | n if n < 0 => None, 373 | n => PointerId::new(n as u64 + 1), 374 | }, 375 | persistent_device_id: PersistentDeviceId::new(self.device_id(env) as u64), 376 | pointer_type: match tool_type { 377 | ToolType::Mouse => PointerType::Mouse, 378 | ToolType::Finger => PointerType::Touch, 379 | ToolType::Stylus | ToolType::Eraser => PointerType::Pen, 380 | _ => PointerType::Unknown, 381 | }, 382 | }; 383 | let buttons = { 384 | let mut pb = PointerButtons::default(); 385 | let bs = self.button_state(env); 386 | if bs.primary() { 387 | pb |= PointerButton::Primary; 388 | } 389 | if bs.stylus_primary() { 390 | pb |= if tool_type == ToolType::Eraser { 391 | PointerButton::PenEraser 392 | } else { 393 | PointerButton::Primary 394 | }; 395 | } 396 | if bs.secondary() || bs.stylus_secondary() { 397 | pb |= PointerButton::Secondary; 398 | } 399 | if bs.teriary() { 400 | pb |= PointerButton::Auxiliary; 401 | } 402 | if bs.back() { 403 | pb |= PointerButton::X1; 404 | } 405 | if bs.forward() { 406 | pb |= PointerButton::X2; 407 | } 408 | // TODO: verify this behavior. 409 | if tool_type == ToolType::Eraser && self.axis(env, Axis::Pressure, action_index) > 0.0 { 410 | pb |= PointerButton::PenEraser; 411 | } 412 | pb 413 | }; 414 | let modifiers = meta_state_to_modifiers(self.meta_state(env)); 415 | let orientation = if matches!(tool_type, ToolType::Stylus | ToolType::Eraser) { 416 | use core::f32::consts::FRAC_PI_2; 417 | let axis_orientation = self.axis(env, Axis::Orientation, action_index); 418 | let axis_tilt = self.axis(env, Axis::Tilt, action_index); 419 | let altitude = FRAC_PI_2 - axis_tilt; 420 | let azimuth = (-axis_orientation + 3.0 * FRAC_PI_2).rem_euclid(4.0 * FRAC_PI_2); 421 | PointerOrientation { altitude, azimuth } 422 | } else { 423 | Default::default() 424 | }; 425 | let contact_geometry = if pointer.pointer_type == PointerType::Touch { 426 | let height = self.axis(env, Axis::TouchMajor, action_index) as f64; 427 | let width = self.axis(env, Axis::TouchMinor, action_index) as f64; 428 | (height > 0.0 && width > 0.0) 429 | .then_some(ContactGeometry { width, height }) 430 | .unwrap_or_default() 431 | } else { 432 | Default::default() 433 | }; 434 | let state = PointerState { 435 | time, 436 | position: PhysicalPosition:: { 437 | x: self.axis(env, Axis::X, action_index) as f64, 438 | y: self.axis(env, Axis::Y, action_index) as f64, 439 | }, 440 | buttons, 441 | // `TapCounter` will attach an appropriate count. 442 | count: 0, 443 | modifiers, 444 | contact_geometry, 445 | orientation, 446 | pressure: self.axis(env, Axis::Pressure, action_index) * 0.5, 447 | tangential_pressure: 0.0, 448 | }; 449 | 450 | let button = { 451 | // Button constants from . 452 | const BUTTON_PRIMARY: jint = 0b1; 453 | const BUTTON_STYLUS_PRIMARY: jint = 0b100000; 454 | const BUTTON_SECONDARY: jint = 0b10; 455 | const BUTTON_STYLUS_SECONDARY: jint = 0b1000000; 456 | const BUTTON_TERTIARY: jint = 0b100; 457 | const BUTTON_BACK: jint = 0b1000; 458 | const BUTTON_FORWARD: jint = 0b10000; 459 | match self.action_button(env) { 460 | BUTTON_PRIMARY | BUTTON_STYLUS_PRIMARY => Some(PointerButton::Primary), 461 | BUTTON_SECONDARY | BUTTON_STYLUS_SECONDARY => Some(PointerButton::Secondary), 462 | BUTTON_TERTIARY => Some(PointerButton::Auxiliary), 463 | BUTTON_BACK => Some(PointerButton::X1), 464 | BUTTON_FORWARD => Some(PointerButton::X2), 465 | _ => (tool_type == ToolType::Eraser).then_some(PointerButton::PenEraser), 466 | } 467 | }; 468 | 469 | Some(match action { 470 | MotionAction::Down | MotionAction::PointerDown => PointerEvent::Down { 471 | pointer, 472 | state, 473 | button, 474 | }, 475 | MotionAction::Up | MotionAction::PointerUp => PointerEvent::Up { 476 | pointer, 477 | state, 478 | button, 479 | }, 480 | MotionAction::Move | MotionAction::HoverMove => { 481 | let hsz = self.history_size(env); 482 | let mut coalesced: Vec = vec![state.clone(); hsz as usize]; 483 | for pos in 0..hsz { 484 | let i = pos as usize; 485 | coalesced[i].time = self.historical_event_time_nanos(env, pos) as u64; 486 | coalesced[i].position = PhysicalPosition:: { 487 | x: self.historical_axis(env, Axis::X, action_index, pos) as f64, 488 | y: self.historical_axis(env, Axis::Y, action_index, pos) as f64, 489 | }; 490 | coalesced[i].contact_geometry = if pointer.pointer_type == PointerType::Touch { 491 | let height = 492 | self.historical_axis(env, Axis::TouchMajor, action_index, pos) as f64; 493 | let width = 494 | self.historical_axis(env, Axis::TouchMinor, action_index, pos) as f64; 495 | (height > 0.0 && width > 0.0) 496 | .then_some(ContactGeometry { width, height }) 497 | .unwrap_or_default() 498 | } else { 499 | Default::default() 500 | }; 501 | coalesced[i].pressure = 502 | self.historical_axis(env, Axis::Pressure, action_index, pos) * 0.5; 503 | coalesced[i].orientation = 504 | if matches!(tool_type, ToolType::Stylus | ToolType::Eraser) { 505 | use core::f32::consts::FRAC_PI_2; 506 | let axis_orientation = 507 | self.historical_axis(env, Axis::Orientation, action_index, pos); 508 | let axis_tilt = 509 | self.historical_axis(env, Axis::Tilt, action_index, pos); 510 | let altitude = FRAC_PI_2 - axis_tilt; 511 | let azimuth = 512 | (-axis_orientation + 3.0 * FRAC_PI_2).rem_euclid(4.0 * FRAC_PI_2); 513 | PointerOrientation { altitude, azimuth } 514 | } else { 515 | Default::default() 516 | }; 517 | } 518 | 519 | PointerEvent::Move(PointerUpdate { 520 | pointer, 521 | current: state, 522 | coalesced, 523 | // TODO: map predicted events 524 | predicted: vec![], 525 | }) 526 | } 527 | MotionAction::Cancel => PointerEvent::Cancel(pointer), 528 | MotionAction::HoverEnter => PointerEvent::Enter(pointer), 529 | MotionAction::HoverExit => PointerEvent::Leave(pointer), 530 | MotionAction::Scroll => PointerEvent::Scroll { 531 | pointer, 532 | delta: ScrollDelta::PixelDelta(PhysicalPosition:: { 533 | x: (self.axis(env, Axis::Hscroll, action_index) 534 | * vc.scaled_horizontal_scroll_factor) as f64, 535 | y: (self.axis(env, Axis::Vscroll, action_index) 536 | * vc.scaled_vertical_scroll_factor) as f64, 537 | }), 538 | state, 539 | }, 540 | _ => { 541 | // Other current `MotionAction` values relate to gamepad/joystick buttons; 542 | // ui-events doesn't currently have types for these, so consider them unhandled. 543 | return None; 544 | } 545 | }) 546 | } 547 | } 548 | 549 | /// Convert `MetaState` to `Modifiers`. 550 | fn meta_state_to_modifiers(s: MetaState) -> Modifiers { 551 | let mut m = Modifiers::default(); 552 | if s.caps_lock_on() { 553 | m |= Modifiers::CAPS_LOCK; 554 | } 555 | if s.scroll_lock_on() { 556 | m |= Modifiers::SCROLL_LOCK; 557 | } 558 | if s.num_lock_on() { 559 | m |= Modifiers::NUM_LOCK; 560 | } 561 | if s.sym_on() { 562 | m |= Modifiers::SYMBOL; 563 | } 564 | if s.shift_on() { 565 | m |= Modifiers::SHIFT; 566 | } 567 | if s.alt_on() { 568 | m |= Modifiers::ALT; 569 | } 570 | if s.ctrl_on() { 571 | m |= Modifiers::CONTROL; 572 | } 573 | if s.function_on() { 574 | m |= Modifiers::FN; 575 | } 576 | if s.meta_on() { 577 | m |= Modifiers::META; 578 | } 579 | m 580 | } 581 | 582 | /// State related to detecting taps for tap counting. 583 | #[derive(Clone, Debug)] 584 | struct TapState { 585 | /// Pointer ID associated, used for attaching counts 586 | /// to `PointerEvent::Move` and `PointerEvent::Up`. 587 | /// Ignored for tap counting, because pointer id can 588 | /// change between taps in a multi-tap. 589 | pointer_id: Option, 590 | /// Nanosecond timestamp when the tap went Down. 591 | down_time: u64, 592 | /// Nanosecond timestamp when the tap went Up. 593 | /// 594 | /// Resets to `down_time` when tap goes Down. 595 | up_time: u64, 596 | /// The local tap count as of the last Down phase. 597 | count: u8, 598 | /// x coordinate. 599 | x: f64, 600 | /// y coordinate. 601 | y: f64, 602 | } 603 | 604 | /// Track and apply tap counts for `PointerEvent`. 605 | #[derive(Default)] 606 | pub struct TapCounter { 607 | /// The `ViewConfiguration` which configures tap counting. 608 | pub vc: ViewConfiguration, 609 | /// Recent taps which can be used for tap counting. 610 | taps: Vec, 611 | } 612 | 613 | impl TapCounter { 614 | /// Make a new `TapCounter` with `ViewConfiguration` from your view. 615 | pub fn new(vc: ViewConfiguration) -> Self { 616 | Self { vc, taps: vec![] } 617 | } 618 | 619 | /// Enhance a `PointerEvent` with `count`. 620 | /// 621 | pub fn attach_count(&mut self, e: PointerEvent) -> PointerEvent { 622 | match e { 623 | PointerEvent::Down { 624 | button, 625 | pointer, 626 | state, 627 | } => { 628 | let e = if let Some(i) = 629 | self.taps.iter().position(|TapState { x, y, up_time, .. }| { 630 | let dx = (x - state.position.x).abs(); 631 | let dy = (y - state.position.y).abs(); 632 | (dx * dx + dy * dy).sqrt() < self.vc.scaled_double_tap_slop as f64 633 | && (up_time + self.vc.multi_press_timeout.max(400) as u64 * 1000000) 634 | > state.time 635 | }) { 636 | let count = self.taps[i].count + 1; 637 | self.taps[i].count = count; 638 | self.taps[i].pointer_id = pointer.pointer_id; 639 | self.taps[i].down_time = state.time; 640 | self.taps[i].up_time = state.time; 641 | self.taps[i].x = state.position.x; 642 | self.taps[i].y = state.position.y; 643 | 644 | PointerEvent::Down { 645 | button, 646 | pointer, 647 | state: PointerState { count, ..state }, 648 | } 649 | } else { 650 | let s = TapState { 651 | pointer_id: pointer.pointer_id, 652 | down_time: state.time, 653 | up_time: state.time, 654 | count: 1, 655 | x: state.position.x, 656 | y: state.position.y, 657 | }; 658 | self.taps.push(s); 659 | PointerEvent::Down { 660 | button, 661 | pointer, 662 | state: PointerState { count: 1, ..state }, 663 | } 664 | }; 665 | self.clear_expired(state.time); 666 | e 667 | } 668 | PointerEvent::Up { 669 | button, 670 | pointer, 671 | ref state, 672 | } => { 673 | if let Some(i) = self 674 | .taps 675 | .iter() 676 | .position(|TapState { pointer_id, .. }| *pointer_id == pointer.pointer_id) 677 | { 678 | self.taps[i].up_time = state.time; 679 | PointerEvent::Up { 680 | button, 681 | pointer, 682 | state: PointerState { 683 | count: self.taps[i].count, 684 | ..state.clone() 685 | }, 686 | } 687 | } else { 688 | e.clone() 689 | } 690 | } 691 | PointerEvent::Move(PointerUpdate { 692 | pointer, 693 | ref current, 694 | ref coalesced, 695 | ref predicted, 696 | }) => { 697 | if let Some(TapState { count, .. }) = self 698 | .taps 699 | .iter() 700 | .find( 701 | |TapState { 702 | pointer_id, 703 | down_time, 704 | up_time, 705 | .. 706 | }| { 707 | *pointer_id == pointer.pointer_id && down_time == up_time 708 | }, 709 | ) 710 | .cloned() 711 | { 712 | PointerEvent::Move(PointerUpdate { 713 | pointer, 714 | current: PointerState { 715 | count, 716 | ..current.clone() 717 | }, 718 | coalesced: coalesced 719 | .iter() 720 | .cloned() 721 | .map(|u| PointerState { count, ..u }) 722 | .collect(), 723 | predicted: predicted 724 | .iter() 725 | .cloned() 726 | .map(|u| PointerState { count, ..u }) 727 | .collect(), 728 | }) 729 | } else { 730 | e 731 | } 732 | } 733 | PointerEvent::Cancel(p) | PointerEvent::Leave(p) => { 734 | self.taps 735 | .retain(|TapState { pointer_id, .. }| *pointer_id != p.pointer_id); 736 | e.clone() 737 | } 738 | PointerEvent::Enter(..) | PointerEvent::Scroll { .. } => e.clone(), 739 | } 740 | } 741 | 742 | /// Clear expired taps. 743 | /// 744 | /// `t` is the time of the last received event. 745 | /// All events have the same time base on Android, so this is valid here. 746 | fn clear_expired(&mut self, t: u64) { 747 | self.taps.retain( 748 | |TapState { 749 | down_time, up_time, .. 750 | }| { 751 | down_time == up_time 752 | || (up_time + self.vc.multi_press_timeout.max(400) as u64 * 1000000) > t 753 | }, 754 | ); 755 | } 756 | } 757 | -------------------------------------------------------------------------------- /src/graphics.rs: -------------------------------------------------------------------------------- 1 | use jni::{JNIEnv, objects::JObject, sys::jint}; 2 | 3 | #[repr(transparent)] 4 | pub struct Rect<'local>(pub JObject<'local>); 5 | 6 | impl<'local> Rect<'local> { 7 | pub fn left(&self, env: &mut JNIEnv<'local>) -> jint { 8 | env.get_field(&self.0, "left", "I").unwrap().i().unwrap() 9 | } 10 | 11 | pub fn top(&self, env: &mut JNIEnv<'local>) -> jint { 12 | env.get_field(&self.0, "top", "I").unwrap().i().unwrap() 13 | } 14 | 15 | pub fn right(&self, env: &mut JNIEnv<'local>) -> jint { 16 | env.get_field(&self.0, "right", "I").unwrap().i().unwrap() 17 | } 18 | 19 | pub fn bottom(&self, env: &mut JNIEnv<'local>) -> jint { 20 | env.get_field(&self.0, "bottom", "I").unwrap().i().unwrap() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ime.rs: -------------------------------------------------------------------------------- 1 | use jni::{ 2 | JNIEnv, 3 | objects::{JObject, JString}, 4 | sys::{JNI_TRUE, jboolean, jint, jlong}, 5 | }; 6 | use std::borrow::Cow; 7 | 8 | use crate::{binder::*, callback_ctx::*, events::KeyEvent, util::*, view::*}; 9 | 10 | pub const INPUT_TYPE_MASK_CLASS: u32 = 0x0000000f; 11 | pub const INPUT_TYPE_MASK_VARIATION: u32 = 0x00000ff0; 12 | pub const INPUT_TYPE_MASK_FLAGS: u32 = 0x00fff000; 13 | pub const INPUT_TYPE_NULL: u32 = 0x00000000; 14 | pub const INPUT_TYPE_CLASS_TEXT: u32 = 0x00000001; 15 | pub const INPUT_TYPE_TEXT_FLAG_CAP_CHARACTERS: u32 = 0x00001000; 16 | pub const INPUT_TYPE_TEXT_FLAG_CAP_WORDS: u32 = 0x00002000; 17 | pub const INPUT_TYPE_TEXT_FLAG_CAP_SENTENCES: u32 = 0x00004000; 18 | pub const INPUT_TYPE_TEXT_FLAG_AUTO_CORRECT: u32 = 0x00008000; 19 | pub const INPUT_TYPE_TEXT_FLAG_AUTO_COMPLETE: u32 = 0x00010000; 20 | pub const INPUT_TYPE_TEXT_FLAG_MULTI_LINE: u32 = 0x00020000; 21 | pub const INPUT_TYPE_TEXT_FLAG_IME_MULTI_LINE: u32 = 0x00040000; 22 | pub const INPUT_TYPE_TEXT_FLAG_NO_SUGGESTIONS: u32 = 0x00080000; 23 | pub const INPUT_TYPE_TEXT_FLAG_ENABLE_TEXT_CONVERSION_SUGGESTIONS: u32 = 0x00100000; 24 | pub const INPUT_TYPE_TEXT_VARIATION_NORMAL: u32 = 0x00000000; 25 | pub const INPUT_TYPE_TEXT_VARIATION_URI: u32 = 0x00000010; 26 | pub const INPUT_TYPE_TEXT_VARIATION_EMAIL_ADDRESS: u32 = 0x00000020; 27 | pub const INPUT_TYPE_TEXT_VARIATION_EMAIL_SUBJECT: u32 = 0x00000030; 28 | pub const INPUT_TYPE_TEXT_VARIATION_SHORT_MESSAGE: u32 = 0x00000040; 29 | pub const INPUT_TYPE_TEXT_VARIATION_LONG_MESSAGE: u32 = 0x00000050; 30 | pub const INPUT_TYPE_TEXT_VARIATION_PERSON_NAME: u32 = 0x00000060; 31 | pub const INPUT_TYPE_TEXT_VARIATION_POSTAL_ADDRESS: u32 = 0x00000070; 32 | pub const INPUT_TYPE_TEXT_VARIATION_PASSWORD: u32 = 0x00000080; 33 | pub const INPUT_TYPE_TEXT_VARIATION_VISIBLE_PASSWORD: u32 = 0x00000090; 34 | pub const INPUT_TYPE_TEXT_VARIATION_WEB_EDIT_TEXT: u32 = 0x000000a0; 35 | pub const INPUT_TYPE_TEXT_VARIATION_FILTER: u32 = 0x000000b0; 36 | pub const INPUT_TYPE_TEXT_VARIATION_PHONETIC: u32 = 0x000000c0; 37 | pub const INPUT_TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS: u32 = 0x000000d0; 38 | pub const INPUT_TYPE_TEXT_VARIATION_WEB_PASSWORD: u32 = 0x000000e0; 39 | pub const INPUT_TYPE_CLASS_NUMBER: u32 = 0x00000002; 40 | pub const INPUT_TYPE_NUMBER_FLAG_SIGNED: u32 = 0x00001000; 41 | pub const INPUT_TYPE_NUMBER_FLAG_DECIMAL: u32 = 0x00002000; 42 | pub const INPUT_TYPE_NUMBER_VARIATION_NORMAL: u32 = 0x00000000; 43 | pub const INPUT_TYPE_NUMBER_VARIATION_PASSWORD: u32 = 0x00000010; 44 | pub const INPUT_TYPE_CLASS_PHONE: u32 = 0x00000003; 45 | pub const INPUT_TYPE_CLASS_DATETIME: u32 = 0x00000004; 46 | pub const INPUT_TYPE_DATETIME_VARIATION_NORMAL: u32 = 0x00000000; 47 | pub const INPUT_TYPE_DATETIME_VARIATION_DATE: u32 = 0x00000010; 48 | pub const INPUT_TYPE_DATETIME_VARIATION_TIME: u32 = 0x00000020; 49 | 50 | pub const IME_FLAG_NO_PERSONALIZED_LEARNING: u32 = 0x1000000; 51 | pub const IME_FLAG_NO_FULLSCREEN: u32 = 0x2000000; 52 | pub const IME_FLAG_NAVIGATE_PREVIOUS: u32 = 0x4000000; 53 | pub const IME_FLAG_NAVIGATE_NEXT: u32 = 0x8000000; 54 | pub const IME_FLAG_NO_EXTRACT_UI: u32 = 0x10000000; 55 | pub const IME_FLAG_NO_ACCESSORY_ACTION: u32 = 0x20000000; 56 | pub const IME_FLAG_NO_ENTER_ACTION: u32 = 0x40000000; 57 | pub const IME_FLAG_FORCE_ASCII: u32 = 0x80000000; 58 | 59 | pub const CAP_MODE_CHARACTERS: u32 = INPUT_TYPE_TEXT_FLAG_CAP_CHARACTERS; 60 | pub const CAP_MODE_WORDS: u32 = INPUT_TYPE_TEXT_FLAG_CAP_WORDS; 61 | pub const CAP_MODE_SENTENCES: u32 = INPUT_TYPE_TEXT_FLAG_CAP_SENTENCES; 62 | 63 | #[repr(transparent)] 64 | pub struct InputMethodManager<'local>(pub JObject<'local>); 65 | 66 | impl<'local> InputMethodManager<'local> { 67 | pub fn show_soft_input( 68 | &self, 69 | env: &mut JNIEnv<'local>, 70 | view: &View<'local>, 71 | flags: jint, 72 | ) -> bool { 73 | env.call_method( 74 | &self.0, 75 | "showSoftInput", 76 | "(Landroid/view/View;I)Z", 77 | &[(&view.0).into(), flags.into()], 78 | ) 79 | .unwrap() 80 | .z() 81 | .unwrap() 82 | } 83 | 84 | pub fn hide_soft_input_from_window( 85 | &self, 86 | env: &mut JNIEnv<'local>, 87 | window_token: &IBinder<'local>, 88 | flags: jint, 89 | ) -> bool { 90 | env.call_method( 91 | &self.0, 92 | "hideSoftInputFromWindow", 93 | "(Landroid/os/IBinder;I)Z", 94 | &[(&window_token.0).into(), flags.into()], 95 | ) 96 | .unwrap() 97 | .z() 98 | .unwrap() 99 | } 100 | 101 | pub fn restart_input(&self, env: &mut JNIEnv<'local>, view: &View<'local>) { 102 | env.call_method( 103 | &self.0, 104 | "restartInput", 105 | "(Landroid/view/View;)V", 106 | &[(&view.0).into()], 107 | ) 108 | .unwrap() 109 | .v() 110 | .unwrap(); 111 | } 112 | 113 | pub fn update_selection( 114 | &self, 115 | env: &mut JNIEnv<'local>, 116 | view: &View<'local>, 117 | sel_start: jint, 118 | sel_end: jint, 119 | candidates_start: jint, 120 | candidates_end: jint, 121 | ) { 122 | env.call_method( 123 | &self.0, 124 | "updateSelection", 125 | "(Landroid/view/View;IIII)V", 126 | &[ 127 | (&view.0).into(), 128 | sel_start.into(), 129 | sel_end.into(), 130 | candidates_start.into(), 131 | candidates_end.into(), 132 | ], 133 | ) 134 | .unwrap() 135 | .v() 136 | .unwrap(); 137 | } 138 | } 139 | 140 | #[repr(transparent)] 141 | pub struct EditorInfo<'local>(pub JObject<'local>); 142 | 143 | impl<'local> EditorInfo<'local> { 144 | pub fn set_input_type(&self, env: &mut JNIEnv<'local>, value: u32) { 145 | env.set_field(&self.0, "inputType", "I", (value as jint).into()) 146 | .unwrap(); 147 | } 148 | 149 | pub fn set_ime_options(&self, env: &mut JNIEnv<'local>, value: u32) { 150 | env.set_field(&self.0, "imeOptions", "I", (value as jint).into()) 151 | .unwrap(); 152 | } 153 | 154 | pub fn set_initial_sel_start(&self, env: &mut JNIEnv<'local>, value: jint) { 155 | env.set_field(&self.0, "initialSelStart", "I", value.into()) 156 | .unwrap(); 157 | } 158 | 159 | pub fn set_initial_sel_end(&self, env: &mut JNIEnv<'local>, value: jint) { 160 | env.set_field(&self.0, "initialSelEnd", "I", value.into()) 161 | .unwrap(); 162 | } 163 | 164 | pub fn set_initial_caps_mode(&self, env: &mut JNIEnv<'local>, value: u32) { 165 | env.set_field(&self.0, "initialCapsMode", "I", (value as jint).into()) 166 | .unwrap(); 167 | } 168 | } 169 | 170 | #[allow(unused_variables)] 171 | pub trait InputConnection { 172 | fn on_create_input_connection<'local>( 173 | &mut self, 174 | ctx: &mut CallbackCtx<'local>, 175 | out_attrs: &EditorInfo<'local>, 176 | ); 177 | 178 | fn text_before_cursor<'slf>( 179 | &'slf mut self, 180 | ctx: &mut CallbackCtx, 181 | n: jint, 182 | ) -> Option>; 183 | // TODO: styled version 184 | 185 | fn text_after_cursor<'slf>( 186 | &'slf mut self, 187 | ctx: &mut CallbackCtx, 188 | n: jint, 189 | ) -> Option>; 190 | // TODO: styled version 191 | 192 | fn selected_text<'slf>(&'slf mut self, ctx: &mut CallbackCtx) -> Option>; 193 | // TODO: styled version 194 | 195 | fn cursor_caps_mode(&mut self, ctx: &mut CallbackCtx, req_modes: u32) -> u32; 196 | 197 | // TODO: Do we need to bind getExtractedText? Gio's InputConnection 198 | // just returns null. 199 | 200 | fn delete_surrounding_text( 201 | &mut self, 202 | ctx: &mut CallbackCtx, 203 | before_length: jint, 204 | after_length: jint, 205 | ) -> bool; 206 | 207 | fn delete_surrounding_text_in_code_points( 208 | &mut self, 209 | ctx: &mut CallbackCtx, 210 | before_length: jint, 211 | after_length: jint, 212 | ) -> bool; 213 | 214 | fn set_composing_text( 215 | &mut self, 216 | ctx: &mut CallbackCtx, 217 | text: &str, 218 | new_cursor_position: jint, 219 | ) -> bool; 220 | // TODO: styled version 221 | 222 | fn set_composing_region(&mut self, ctx: &mut CallbackCtx, start: jint, end: jint) -> bool; 223 | 224 | fn finish_composing_text(&mut self, ctx: &mut CallbackCtx) -> bool; 225 | 226 | fn commit_text( 227 | &mut self, 228 | ctx: &mut CallbackCtx, 229 | text: &str, 230 | new_cursor_position: jint, 231 | ) -> bool { 232 | self.set_composing_text(ctx, text, new_cursor_position) && self.finish_composing_text(ctx) 233 | } 234 | // TODO: styled version 235 | 236 | // TODO: Do we need to bind commitCompletion or commitCoorrection? 237 | // Gio's InputConnection just returns false for both. 238 | 239 | fn set_selection(&mut self, ctx: &mut CallbackCtx, start: jint, end: jint) -> bool; 240 | 241 | fn perform_editor_action(&mut self, ctx: &mut CallbackCtx, editor_action: jint) -> bool; 242 | 243 | fn perform_context_menu_action(&mut self, ctx: &mut CallbackCtx, id: jint) -> bool { 244 | false 245 | } 246 | 247 | fn begin_batch_edit(&mut self, ctx: &mut CallbackCtx) -> bool; 248 | 249 | fn end_batch_edit(&mut self, ctx: &mut CallbackCtx) -> bool; 250 | 251 | fn send_key_event<'local>( 252 | &mut self, 253 | ctx: &mut CallbackCtx<'local>, 254 | event: &KeyEvent<'local>, 255 | ) -> bool; 256 | 257 | fn clear_meta_key_states(&mut self, ctx: &mut CallbackCtx, states: jint) -> bool { 258 | false 259 | } 260 | 261 | fn report_fullscreen_mode(&mut self, ctx: &mut CallbackCtx, enabled: bool) -> bool { 262 | false 263 | } 264 | 265 | // TODO: Do we need to bind performPrivateCommand? Gio's InputConnection 266 | // just returns false. 267 | 268 | fn request_cursor_updates(&mut self, ctx: &mut CallbackCtx, cursor_update_mode: jint) -> bool; 269 | 270 | fn close_connection(&mut self, ctx: &mut CallbackCtx) {} 271 | 272 | // TODO: Do we need to bind commitContent? Gio's InputConnection 273 | // just returns false. 274 | } 275 | 276 | fn with_input_connection<'local, F, T: Default>( 277 | env: JNIEnv<'local>, 278 | view: View<'local>, 279 | id: jlong, 280 | f: F, 281 | ) -> T 282 | where 283 | F: FnOnce(&mut CallbackCtx<'local>, &mut dyn InputConnection) -> T, 284 | { 285 | with_peer(env, view, id, |ctx, peer| { 286 | let Some(ic) = peer.as_input_connection() else { 287 | return T::default(); 288 | }; 289 | f(ctx, ic) 290 | }) 291 | } 292 | 293 | pub(crate) extern "system" fn on_create_input_connection<'local>( 294 | env: JNIEnv<'local>, 295 | view: View<'local>, 296 | peer: jlong, 297 | out_attrs: EditorInfo<'local>, 298 | ) -> jboolean { 299 | as_jboolean(with_input_connection(env, view, peer, |ctx, ic| { 300 | ic.on_create_input_connection(ctx, &out_attrs); 301 | true 302 | })) 303 | } 304 | 305 | pub(crate) extern "system" fn get_text_before_cursor<'local>( 306 | env: JNIEnv<'local>, 307 | view: View<'local>, 308 | peer: jlong, 309 | n: jint, 310 | ) -> JString<'local> { 311 | with_input_connection(env, view, peer, |ctx, ic| { 312 | if let Some(result) = ic.text_before_cursor(ctx, n) { 313 | ctx.env.new_string(result).unwrap() 314 | } else { 315 | JObject::null().into() 316 | } 317 | }) 318 | } 319 | 320 | pub(crate) extern "system" fn get_text_after_cursor<'local>( 321 | env: JNIEnv<'local>, 322 | view: View<'local>, 323 | peer: jlong, 324 | n: jint, 325 | ) -> JString<'local> { 326 | with_input_connection(env, view, peer, |ctx, ic| { 327 | if let Some(result) = ic.text_after_cursor(ctx, n) { 328 | ctx.env.new_string(result).unwrap() 329 | } else { 330 | JObject::null().into() 331 | } 332 | }) 333 | } 334 | 335 | pub(crate) extern "system" fn get_selected_text<'local>( 336 | env: JNIEnv<'local>, 337 | view: View<'local>, 338 | peer: jlong, 339 | ) -> JString<'local> { 340 | with_input_connection(env, view, peer, |ctx, ic| { 341 | if let Some(result) = ic.selected_text(ctx) { 342 | ctx.env.new_string(result).unwrap() 343 | } else { 344 | JObject::null().into() 345 | } 346 | }) 347 | } 348 | 349 | pub(crate) extern "system" fn get_cursor_caps_mode<'local>( 350 | env: JNIEnv<'local>, 351 | view: View<'local>, 352 | peer: jlong, 353 | req_modes: jint, 354 | ) -> jint { 355 | with_input_connection(env, view, peer, |ctx, ic| { 356 | ic.cursor_caps_mode(ctx, req_modes as u32) as jint 357 | }) 358 | } 359 | 360 | pub(crate) extern "system" fn delete_surrounding_text<'local>( 361 | env: JNIEnv<'local>, 362 | view: View<'local>, 363 | peer: jlong, 364 | before_length: jint, 365 | after_length: jint, 366 | ) -> jboolean { 367 | as_jboolean(with_input_connection(env, view, peer, |ctx, ic| { 368 | ic.delete_surrounding_text(ctx, before_length, after_length) 369 | })) 370 | } 371 | 372 | pub(crate) extern "system" fn delete_surrounding_text_in_code_points<'local>( 373 | env: JNIEnv<'local>, 374 | view: View<'local>, 375 | peer: jlong, 376 | before_length: jint, 377 | after_length: jint, 378 | ) -> jboolean { 379 | as_jboolean(with_input_connection(env, view, peer, |ctx, ic| { 380 | ic.delete_surrounding_text_in_code_points(ctx, before_length, after_length) 381 | })) 382 | } 383 | 384 | pub(crate) extern "system" fn set_composing_text<'local>( 385 | env: JNIEnv<'local>, 386 | view: View<'local>, 387 | peer: jlong, 388 | text: JString<'local>, 389 | new_cursor_position: jint, 390 | ) -> jboolean { 391 | as_jboolean(with_input_connection(env, view, peer, |ctx, ic| { 392 | let text = ctx.env.get_string(&text).unwrap(); 393 | let text = Cow::from(&text); 394 | ic.set_composing_text(ctx, &text, new_cursor_position) 395 | })) 396 | } 397 | 398 | pub(crate) extern "system" fn set_composing_region<'local>( 399 | env: JNIEnv<'local>, 400 | view: View<'local>, 401 | peer: jlong, 402 | start: jint, 403 | end: jint, 404 | ) -> jboolean { 405 | as_jboolean(with_input_connection(env, view, peer, |ctx, ic| { 406 | ic.set_composing_region(ctx, start, end) 407 | })) 408 | } 409 | 410 | pub(crate) extern "system" fn finish_composing_text<'local>( 411 | env: JNIEnv<'local>, 412 | view: View<'local>, 413 | peer: jlong, 414 | ) -> jboolean { 415 | as_jboolean(with_input_connection(env, view, peer, |ctx, ic| { 416 | ic.finish_composing_text(ctx) 417 | })) 418 | } 419 | 420 | pub(crate) extern "system" fn commit_text<'local>( 421 | env: JNIEnv<'local>, 422 | view: View<'local>, 423 | peer: jlong, 424 | text: JString<'local>, 425 | new_cursor_position: jint, 426 | ) -> jboolean { 427 | as_jboolean(with_input_connection(env, view, peer, |ctx, ic| { 428 | let text = ctx.env.get_string(&text).unwrap(); 429 | let text = Cow::from(&text); 430 | ic.commit_text(ctx, &text, new_cursor_position) 431 | })) 432 | } 433 | 434 | pub(crate) extern "system" fn set_selection<'local>( 435 | env: JNIEnv<'local>, 436 | view: View<'local>, 437 | peer: jlong, 438 | start: jint, 439 | end: jint, 440 | ) -> jboolean { 441 | as_jboolean(with_input_connection(env, view, peer, |ctx, ic| { 442 | ic.set_selection(ctx, start, end) 443 | })) 444 | } 445 | 446 | pub(crate) extern "system" fn perform_editor_action<'local>( 447 | env: JNIEnv<'local>, 448 | view: View<'local>, 449 | peer: jlong, 450 | editor_action: jint, 451 | ) -> jboolean { 452 | as_jboolean(with_input_connection(env, view, peer, |ctx, ic| { 453 | ic.perform_editor_action(ctx, editor_action) 454 | })) 455 | } 456 | 457 | pub(crate) extern "system" fn perform_context_menu_action<'local>( 458 | env: JNIEnv<'local>, 459 | view: View<'local>, 460 | peer: jlong, 461 | id: jint, 462 | ) -> jboolean { 463 | as_jboolean(with_input_connection(env, view, peer, |ctx, ic| { 464 | ic.perform_context_menu_action(ctx, id) 465 | })) 466 | } 467 | 468 | pub(crate) extern "system" fn begin_batch_edit<'local>( 469 | env: JNIEnv<'local>, 470 | view: View<'local>, 471 | peer: jlong, 472 | ) -> jboolean { 473 | as_jboolean(with_input_connection(env, view, peer, |ctx, ic| { 474 | ic.begin_batch_edit(ctx) 475 | })) 476 | } 477 | 478 | pub(crate) extern "system" fn end_batch_edit<'local>( 479 | env: JNIEnv<'local>, 480 | view: View<'local>, 481 | peer: jlong, 482 | ) -> jboolean { 483 | as_jboolean(with_input_connection(env, view, peer, |ctx, ic| { 484 | ic.end_batch_edit(ctx) 485 | })) 486 | } 487 | 488 | pub(crate) extern "system" fn input_connection_send_key_event<'local>( 489 | env: JNIEnv<'local>, 490 | view: View<'local>, 491 | peer: jlong, 492 | event: KeyEvent<'local>, 493 | ) -> jboolean { 494 | as_jboolean(with_input_connection(env, view, peer, |ctx, ic| { 495 | ic.send_key_event(ctx, &event) 496 | })) 497 | } 498 | 499 | pub(crate) extern "system" fn input_connection_clear_meta_key_states<'local>( 500 | env: JNIEnv<'local>, 501 | view: View<'local>, 502 | peer: jlong, 503 | states: jint, 504 | ) -> jboolean { 505 | as_jboolean(with_input_connection(env, view, peer, |ctx, ic| { 506 | ic.clear_meta_key_states(ctx, states) 507 | })) 508 | } 509 | 510 | pub(crate) extern "system" fn input_connection_report_fullscreen_mode<'local>( 511 | env: JNIEnv<'local>, 512 | view: View<'local>, 513 | peer: jlong, 514 | enabled: jboolean, 515 | ) -> jboolean { 516 | as_jboolean(with_input_connection(env, view, peer, |ctx, ic| { 517 | ic.report_fullscreen_mode(ctx, enabled == JNI_TRUE) 518 | })) 519 | } 520 | 521 | pub(crate) extern "system" fn request_cursor_updates<'local>( 522 | env: JNIEnv<'local>, 523 | view: View<'local>, 524 | peer: jlong, 525 | cursor_update_mode: jint, 526 | ) -> jboolean { 527 | as_jboolean(with_input_connection(env, view, peer, |ctx, ic| { 528 | ic.request_cursor_updates(ctx, cursor_update_mode) 529 | })) 530 | } 531 | 532 | pub(crate) extern "system" fn close_input_connection<'local>( 533 | env: JNIEnv<'local>, 534 | view: View<'local>, 535 | peer: jlong, 536 | ) { 537 | with_input_connection(env, view, peer, |ctx, ic| { 538 | ic.close_connection(ctx); 539 | }) 540 | } 541 | 542 | pub fn caps_mode(env: &mut JNIEnv, text: &str, off: usize, req_modes: u32) -> u32 { 543 | let text = env.new_string(text).unwrap(); 544 | env.call_static_method( 545 | "android/text/TextUtils", 546 | "getCapsMode", 547 | "(Ljava/lang/CharSequence;II)I", 548 | &[ 549 | (&text).into(), 550 | (off as jint).into(), 551 | (req_modes as jint).into(), 552 | ], 553 | ) 554 | .unwrap() 555 | .i() 556 | .unwrap() as u32 557 | } 558 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(unsafe_op_in_unsafe_fn)] 2 | 3 | pub use jni; 4 | pub use ndk; 5 | 6 | mod accessibility; 7 | pub use accessibility::*; 8 | mod binder; 9 | pub use binder::*; 10 | mod bundle; 11 | pub use bundle::*; 12 | mod callback_ctx; 13 | pub use callback_ctx::*; 14 | mod context; 15 | pub use context::*; 16 | mod events; 17 | pub use events::*; 18 | mod graphics; 19 | pub use graphics::*; 20 | mod ime; 21 | pub use ime::*; 22 | mod surface; 23 | pub use surface::*; 24 | mod util; 25 | mod view; 26 | pub use view::*; 27 | mod view_configuration; 28 | pub use view_configuration::*; 29 | -------------------------------------------------------------------------------- /src/surface.rs: -------------------------------------------------------------------------------- 1 | use jni::{JNIEnv, objects::JObject}; 2 | use ndk::native_window::NativeWindow; 3 | 4 | #[repr(transparent)] 5 | pub struct Surface<'local>(pub JObject<'local>); 6 | 7 | impl<'local> Surface<'local> { 8 | pub fn to_native_window(&self, env: &mut JNIEnv<'local>) -> NativeWindow { 9 | unsafe { NativeWindow::from_surface(env.get_raw(), self.0.as_raw()) }.unwrap() 10 | } 11 | } 12 | 13 | #[repr(transparent)] 14 | pub struct SurfaceHolder<'local>(pub JObject<'local>); 15 | 16 | impl<'local> SurfaceHolder<'local> { 17 | pub fn surface(&self, env: &mut JNIEnv<'local>) -> Surface<'local> { 18 | Surface( 19 | env.call_method(&self.0, "getSurface", "()Landroid/view/Surface;", &[]) 20 | .unwrap() 21 | .l() 22 | .unwrap(), 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use jni::sys::{JNI_FALSE, JNI_TRUE, jboolean}; 2 | 3 | pub(crate) fn as_jboolean(flag: bool) -> jboolean { 4 | if flag { JNI_TRUE } else { JNI_FALSE } 5 | } 6 | -------------------------------------------------------------------------------- /src/view.rs: -------------------------------------------------------------------------------- 1 | use jni::{ 2 | JNIEnv, NativeMethod, 3 | descriptors::Desc, 4 | objects::{JClass, JIntArray, JObject}, 5 | sys::{JNI_TRUE, jboolean, jint, jlong}, 6 | }; 7 | use ndk::event::Keycode; 8 | use num_enum::FromPrimitive; 9 | use send_wrapper::SendWrapper; 10 | use std::{ 11 | cell::RefCell, 12 | collections::BTreeMap, 13 | ffi::c_void, 14 | rc::Rc, 15 | sync::{ 16 | Mutex, Once, 17 | atomic::{AtomicI64, Ordering}, 18 | }, 19 | }; 20 | 21 | use crate::{ 22 | accessibility::*, binder::*, callback_ctx::*, context::*, events::*, graphics::*, ime::*, 23 | surface::*, util::*, view_configuration::*, 24 | }; 25 | 26 | #[repr(transparent)] 27 | pub struct View<'local>(pub JObject<'local>); 28 | 29 | impl<'local> View<'local> { 30 | pub fn post_frame_callback(&self, env: &mut JNIEnv<'local>) { 31 | env.call_method(&self.0, "postFrameCallback", "()V", &[]) 32 | .unwrap() 33 | .v() 34 | .unwrap() 35 | } 36 | 37 | pub fn remove_frame_callback(&self, env: &mut JNIEnv<'local>) { 38 | env.call_method(&self.0, "removeFrameCallback", "()V", &[]) 39 | .unwrap() 40 | .v() 41 | .unwrap() 42 | } 43 | 44 | pub fn post_delayed(&self, env: &mut JNIEnv<'local>, delay_millis: jlong) -> bool { 45 | env.call_method(&self.0, "postDelayed", "(J)Z", &[delay_millis.into()]) 46 | .unwrap() 47 | .z() 48 | .unwrap() 49 | } 50 | 51 | pub fn remove_delayed_callbacks(&self, env: &mut JNIEnv<'local>) -> bool { 52 | env.call_method(&self.0, "removeDelayedCallbacks", "()Z", &[]) 53 | .unwrap() 54 | .z() 55 | .unwrap() 56 | } 57 | 58 | pub fn is_focused(&self, env: &mut JNIEnv<'local>) -> bool { 59 | env.call_method(&self.0, "isFocused", "()Z", &[]) 60 | .unwrap() 61 | .z() 62 | .unwrap() 63 | } 64 | 65 | pub fn input_method_manager(&self, env: &mut JNIEnv<'local>) -> InputMethodManager<'local> { 66 | InputMethodManager( 67 | env.get_field( 68 | &self.0, 69 | "mInputMethodManager", 70 | "Landroid/view/inputmethod/InputMethodManager;", 71 | ) 72 | .unwrap() 73 | .l() 74 | .unwrap(), 75 | ) 76 | } 77 | 78 | pub fn context(&self, env: &mut JNIEnv<'local>) -> Context<'local> { 79 | Context( 80 | env.call_method(&self.0, "getContext", "()Landroid/content/Context;", &[]) 81 | .unwrap() 82 | .l() 83 | .unwrap(), 84 | ) 85 | } 86 | 87 | pub fn view_configuration(&self, env: &mut JNIEnv<'local>) -> ViewConfiguration { 88 | ViewConfiguration::new(&self.0, env) 89 | } 90 | 91 | pub fn window_token(&self, env: &mut JNIEnv<'local>) -> IBinder<'local> { 92 | IBinder( 93 | env.call_method(&self.0, "getWindowToken", "()Landroid/os/IBinder;", &[]) 94 | .unwrap() 95 | .l() 96 | .unwrap(), 97 | ) 98 | } 99 | } 100 | 101 | #[allow(unused_variables)] 102 | pub trait ViewPeer { 103 | fn on_measure( 104 | &mut self, 105 | ctx: &mut CallbackCtx, 106 | width_spec: jint, 107 | height_spec: jint, 108 | ) -> Option<(jint, jint)> { 109 | None 110 | } 111 | 112 | fn on_layout( 113 | &mut self, 114 | ctx: &mut CallbackCtx, 115 | changed: bool, 116 | left: jint, 117 | top: jint, 118 | right: jint, 119 | bottom: jint, 120 | ) { 121 | } 122 | 123 | fn on_size_changed(&mut self, ctx: &mut CallbackCtx, w: jint, h: jint, oldw: jint, oldh: jint) { 124 | } 125 | 126 | fn on_key_down<'local>( 127 | &mut self, 128 | ctx: &mut CallbackCtx<'local>, 129 | key_code: Keycode, 130 | event: &KeyEvent<'local>, 131 | ) -> bool { 132 | false 133 | } 134 | 135 | fn on_key_up<'local>( 136 | &mut self, 137 | ctx: &mut CallbackCtx<'local>, 138 | key_code: Keycode, 139 | event: &KeyEvent<'local>, 140 | ) -> bool { 141 | false 142 | } 143 | 144 | fn on_trackball_event<'local>( 145 | &mut self, 146 | ctx: &mut CallbackCtx<'local>, 147 | event: &MotionEvent<'local>, 148 | ) -> bool { 149 | false 150 | } 151 | 152 | fn on_touch_event<'local>( 153 | &mut self, 154 | ctx: &mut CallbackCtx<'local>, 155 | event: &MotionEvent<'local>, 156 | ) -> bool { 157 | false 158 | } 159 | 160 | fn on_generic_motion_event<'local>( 161 | &mut self, 162 | ctx: &mut CallbackCtx<'local>, 163 | event: &MotionEvent<'local>, 164 | ) -> bool { 165 | false 166 | } 167 | 168 | fn on_hover_event<'local>( 169 | &mut self, 170 | ctx: &mut CallbackCtx<'local>, 171 | event: &MotionEvent<'local>, 172 | ) -> bool { 173 | false 174 | } 175 | 176 | fn on_focus_changed<'local>( 177 | &mut self, 178 | ctx: &mut CallbackCtx<'local>, 179 | gain_focus: bool, 180 | direction: jint, 181 | previously_focused_rect: Option<&Rect<'local>>, 182 | ) { 183 | } 184 | 185 | fn on_window_focus_changed(&mut self, ctx: &mut CallbackCtx, has_window_focus: bool) {} 186 | 187 | fn on_attached_to_window(&mut self, ctx: &mut CallbackCtx) {} 188 | 189 | fn on_detached_from_window(&mut self, ctx: &mut CallbackCtx) {} 190 | 191 | fn on_window_visibility_changed(&mut self, ctx: &mut CallbackCtx, visibility: jint) {} 192 | 193 | fn surface_created<'local>( 194 | &mut self, 195 | ctx: &mut CallbackCtx<'local>, 196 | holder: &SurfaceHolder<'local>, 197 | ) { 198 | } 199 | 200 | fn surface_changed<'local>( 201 | &mut self, 202 | ctx: &mut CallbackCtx<'local>, 203 | holder: &SurfaceHolder<'local>, 204 | format: jint, 205 | width: jint, 206 | height: jint, 207 | ) { 208 | } 209 | 210 | fn surface_destroyed<'local>( 211 | &mut self, 212 | ctx: &mut CallbackCtx<'local>, 213 | holder: &SurfaceHolder<'local>, 214 | ) { 215 | } 216 | 217 | fn do_frame(&mut self, ctx: &mut CallbackCtx, frame_time_nanos: jlong) {} 218 | 219 | fn delayed_callback(&mut self, ctx: &mut CallbackCtx) {} 220 | 221 | fn as_accessibility_node_provider(&mut self) -> Option<&mut dyn AccessibilityNodeProvider> { 222 | None 223 | } 224 | 225 | fn as_input_connection(&mut self) -> Option<&mut dyn InputConnection> { 226 | None 227 | } 228 | } 229 | 230 | static NEXT_PEER_ID: AtomicI64 = AtomicI64::new(0); 231 | static PEER_MAP: Mutex>>>>> = 232 | Mutex::new(BTreeMap::new()); 233 | 234 | pub(crate) fn with_peer<'local, F, T: Default>( 235 | env: JNIEnv<'local>, 236 | view: View<'local>, 237 | id: jlong, 238 | f: F, 239 | ) -> T 240 | where 241 | F: FnOnce(&mut CallbackCtx<'local>, &mut dyn ViewPeer) -> T, 242 | { 243 | let map = PEER_MAP.lock().unwrap(); 244 | let Some(peer) = map.get(&id) else { 245 | return T::default(); 246 | }; 247 | let peer = Rc::clone(&**peer); 248 | drop(map); 249 | let mut peer = peer.borrow_mut(); 250 | let mut ctx = CallbackCtx::new(env, view); 251 | let result = f(&mut ctx, &mut **peer); 252 | drop(peer); 253 | ctx.finish(); 254 | result 255 | } 256 | 257 | extern "system" fn on_measure<'local>( 258 | env: JNIEnv<'local>, 259 | view: View<'local>, 260 | peer: jlong, 261 | width_spec: jint, 262 | height_spec: jint, 263 | ) -> JIntArray<'local> { 264 | with_peer(env, view, peer, |ctx, peer| { 265 | if let Some((width, height)) = peer.on_measure(ctx, width_spec, height_spec) { 266 | let result = ctx.env.new_int_array(2).unwrap(); 267 | ctx.env 268 | .set_int_array_region(&result, 0, &[width, height]) 269 | .unwrap(); 270 | result 271 | } else { 272 | JObject::null().into() 273 | } 274 | }) 275 | } 276 | 277 | extern "system" fn on_layout<'local>( 278 | env: JNIEnv<'local>, 279 | view: View<'local>, 280 | peer: jlong, 281 | changed: jboolean, 282 | left: jint, 283 | top: jint, 284 | right: jint, 285 | bottom: jint, 286 | ) { 287 | with_peer(env, view, peer, |ctx, peer| { 288 | peer.on_layout(ctx, changed == JNI_TRUE, left, top, right, bottom); 289 | }) 290 | } 291 | 292 | extern "system" fn on_size_changed<'local>( 293 | env: JNIEnv<'local>, 294 | view: View<'local>, 295 | peer: jlong, 296 | w: jint, 297 | h: jint, 298 | oldw: jint, 299 | oldh: jint, 300 | ) { 301 | with_peer(env, view, peer, |ctx, peer| { 302 | peer.on_size_changed(ctx, w, h, oldw, oldh); 303 | }) 304 | } 305 | 306 | extern "system" fn on_key_down<'local>( 307 | env: JNIEnv<'local>, 308 | view: View<'local>, 309 | peer: jlong, 310 | key_code: jint, 311 | event: KeyEvent<'local>, 312 | ) -> jboolean { 313 | as_jboolean(with_peer(env, view, peer, |ctx, peer| { 314 | peer.on_key_down(ctx, Keycode::from_primitive(key_code), &event) 315 | })) 316 | } 317 | 318 | extern "system" fn on_key_up<'local>( 319 | env: JNIEnv<'local>, 320 | view: View<'local>, 321 | peer: jlong, 322 | key_code: jint, 323 | event: KeyEvent<'local>, 324 | ) -> jboolean { 325 | as_jboolean(with_peer(env, view, peer, |ctx, peer| { 326 | peer.on_key_up(ctx, Keycode::from_primitive(key_code), &event) 327 | })) 328 | } 329 | 330 | extern "system" fn on_trackball_event<'local>( 331 | env: JNIEnv<'local>, 332 | view: View<'local>, 333 | peer: jlong, 334 | event: MotionEvent<'local>, 335 | ) -> jboolean { 336 | as_jboolean(with_peer(env, view, peer, |ctx, peer| { 337 | peer.on_trackball_event(ctx, &event) 338 | })) 339 | } 340 | 341 | extern "system" fn on_touch_event<'local>( 342 | env: JNIEnv<'local>, 343 | view: View<'local>, 344 | peer: jlong, 345 | event: MotionEvent<'local>, 346 | ) -> jboolean { 347 | as_jboolean(with_peer(env, view, peer, |ctx, peer| { 348 | peer.on_touch_event(ctx, &event) 349 | })) 350 | } 351 | 352 | extern "system" fn on_generic_motion_event<'local>( 353 | env: JNIEnv<'local>, 354 | view: View<'local>, 355 | peer: jlong, 356 | event: MotionEvent<'local>, 357 | ) -> jboolean { 358 | as_jboolean(with_peer(env, view, peer, |ctx, peer| { 359 | peer.on_generic_motion_event(ctx, &event) 360 | })) 361 | } 362 | 363 | extern "system" fn on_hover_event<'local>( 364 | env: JNIEnv<'local>, 365 | view: View<'local>, 366 | peer: jlong, 367 | event: MotionEvent<'local>, 368 | ) -> jboolean { 369 | as_jboolean(with_peer(env, view, peer, |ctx, peer| { 370 | peer.on_hover_event(ctx, &event) 371 | })) 372 | } 373 | 374 | extern "system" fn on_focus_changed<'local>( 375 | env: JNIEnv<'local>, 376 | view: View<'local>, 377 | peer: jlong, 378 | gain_focus: jboolean, 379 | direction: jint, 380 | previously_focused_rect: Rect<'local>, 381 | ) { 382 | with_peer(env, view, peer, |ctx, peer| { 383 | peer.on_focus_changed( 384 | ctx, 385 | gain_focus == JNI_TRUE, 386 | direction, 387 | (!previously_focused_rect.0.as_raw().is_null()).then_some(&previously_focused_rect), 388 | ); 389 | }) 390 | } 391 | 392 | extern "system" fn on_window_focus_changed<'local>( 393 | env: JNIEnv<'local>, 394 | view: View<'local>, 395 | peer: jlong, 396 | has_window_focus: jboolean, 397 | ) { 398 | with_peer(env, view, peer, |ctx, peer| { 399 | peer.on_window_focus_changed(ctx, has_window_focus == JNI_TRUE); 400 | }) 401 | } 402 | 403 | extern "system" fn on_attached_to_window<'local>( 404 | env: JNIEnv<'local>, 405 | view: View<'local>, 406 | peer: jlong, 407 | ) { 408 | with_peer(env, view, peer, |ctx, peer| { 409 | peer.on_attached_to_window(ctx); 410 | }) 411 | } 412 | 413 | extern "system" fn on_detached_from_window<'local>( 414 | env: JNIEnv<'local>, 415 | view: View<'local>, 416 | peer: jlong, 417 | ) { 418 | let mut map = PEER_MAP.lock().unwrap(); 419 | let peer = map.remove(&peer).unwrap(); 420 | drop(map); 421 | let mut peer = peer.borrow_mut(); 422 | let mut ctx = CallbackCtx::new(env, view); 423 | peer.on_detached_from_window(&mut ctx); 424 | drop(peer); 425 | ctx.view.remove_frame_callback(&mut ctx.env); 426 | ctx.view.remove_delayed_callbacks(&mut ctx.env); 427 | ctx.finish(); 428 | } 429 | 430 | extern "system" fn on_window_visibility_changed<'local>( 431 | env: JNIEnv<'local>, 432 | view: View<'local>, 433 | peer: jlong, 434 | visibility: jint, 435 | ) { 436 | with_peer(env, view, peer, |ctx, peer| { 437 | peer.on_window_visibility_changed(ctx, visibility); 438 | }) 439 | } 440 | 441 | extern "system" fn surface_created<'local>( 442 | env: JNIEnv<'local>, 443 | view: View<'local>, 444 | peer: jlong, 445 | holder: SurfaceHolder<'local>, 446 | ) { 447 | with_peer(env, view, peer, |ctx, peer| { 448 | peer.surface_created(ctx, &holder); 449 | }) 450 | } 451 | 452 | extern "system" fn surface_changed<'local>( 453 | env: JNIEnv<'local>, 454 | view: View<'local>, 455 | peer: jlong, 456 | holder: SurfaceHolder<'local>, 457 | format: jint, 458 | width: jint, 459 | height: jint, 460 | ) { 461 | with_peer(env, view, peer, |ctx, peer| { 462 | peer.surface_changed(ctx, &holder, format, width, height); 463 | }) 464 | } 465 | 466 | extern "system" fn surface_destroyed<'local>( 467 | env: JNIEnv<'local>, 468 | view: View<'local>, 469 | peer: jlong, 470 | holder: SurfaceHolder<'local>, 471 | ) { 472 | with_peer(env, view, peer, |ctx, peer| { 473 | peer.surface_destroyed(ctx, &holder); 474 | }) 475 | } 476 | 477 | extern "system" fn do_frame<'local>( 478 | env: JNIEnv<'local>, 479 | view: View<'local>, 480 | peer: jlong, 481 | frame_time_nanos: jlong, 482 | ) { 483 | with_peer(env, view, peer, |ctx, peer| { 484 | peer.do_frame(ctx, frame_time_nanos); 485 | }) 486 | } 487 | 488 | extern "system" fn delayed_callback<'local>(env: JNIEnv<'local>, view: View<'local>, peer: jlong) { 489 | with_peer(env, view, peer, |ctx, peer| { 490 | peer.delayed_callback(ctx); 491 | }) 492 | } 493 | 494 | pub fn register_view_peer(peer: impl 'static + ViewPeer) -> jlong { 495 | let id = NEXT_PEER_ID.fetch_add(1, Ordering::Relaxed); 496 | let mut map = PEER_MAP.lock().unwrap(); 497 | map.insert(id, SendWrapper::new(Rc::new(RefCell::new(Box::new(peer))))); 498 | id 499 | } 500 | 501 | pub fn register_view_class<'local, 'other_local>( 502 | env: &mut JNIEnv<'local>, 503 | class: impl Desc<'local, JClass<'other_local>>, 504 | new_peer: for<'a> extern "system" fn(JNIEnv<'a>, View<'a>, Context<'a>) -> jlong, 505 | ) { 506 | static REGISTER_BASE_NATIVES: Once = Once::new(); 507 | REGISTER_BASE_NATIVES.call_once(|| { 508 | env.register_native_methods( 509 | "org/linebender/android/rustview/RustView", 510 | &[ 511 | NativeMethod { 512 | name: "onMeasureNative".into(), 513 | sig: "(JII)[I".into(), 514 | fn_ptr: on_measure as *mut c_void, 515 | }, 516 | NativeMethod { 517 | name: "onLayoutNative".into(), 518 | sig: "(JZIIII)V".into(), 519 | fn_ptr: on_layout as *mut c_void, 520 | }, 521 | NativeMethod { 522 | name: "onSizeChangedNative".into(), 523 | sig: "(JIIII)V".into(), 524 | fn_ptr: on_size_changed as *mut c_void, 525 | }, 526 | NativeMethod { 527 | name: "onKeyDownNative".into(), 528 | sig: "(JILandroid/view/KeyEvent;)Z".into(), 529 | fn_ptr: on_key_down as *mut c_void, 530 | }, 531 | NativeMethod { 532 | name: "onKeyUpNative".into(), 533 | sig: "(JILandroid/view/KeyEvent;)Z".into(), 534 | fn_ptr: on_key_up as *mut c_void, 535 | }, 536 | NativeMethod { 537 | name: "onTrackballEventNative".into(), 538 | sig: "(JLandroid/view/MotionEvent;)Z".into(), 539 | fn_ptr: on_trackball_event as *mut c_void, 540 | }, 541 | NativeMethod { 542 | name: "onTouchEventNative".into(), 543 | sig: "(JLandroid/view/MotionEvent;)Z".into(), 544 | fn_ptr: on_touch_event as *mut c_void, 545 | }, 546 | NativeMethod { 547 | name: "onGenericMotionEventNative".into(), 548 | sig: "(JLandroid/view/MotionEvent;)Z".into(), 549 | fn_ptr: on_generic_motion_event as *mut c_void, 550 | }, 551 | NativeMethod { 552 | name: "onHoverEventNative".into(), 553 | sig: "(JLandroid/view/MotionEvent;)Z".into(), 554 | fn_ptr: on_hover_event as *mut c_void, 555 | }, 556 | NativeMethod { 557 | name: "onFocusChangedNative".into(), 558 | sig: "(JZILandroid/graphics/Rect;)V".into(), 559 | fn_ptr: on_focus_changed as *mut c_void, 560 | }, 561 | NativeMethod { 562 | name: "onWindowFocusChangedNative".into(), 563 | sig: "(JZ)V".into(), 564 | fn_ptr: on_window_focus_changed as *mut c_void, 565 | }, 566 | NativeMethod { 567 | name: "onAttachedToWindowNative".into(), 568 | sig: "(J)V".into(), 569 | fn_ptr: on_attached_to_window as *mut c_void, 570 | }, 571 | NativeMethod { 572 | name: "onDetachedFromWindowNative".into(), 573 | sig: "(J)V".into(), 574 | fn_ptr: on_detached_from_window as *mut c_void, 575 | }, 576 | NativeMethod { 577 | name: "onWindowVisibilityChangedNative".into(), 578 | sig: "(JI)V".into(), 579 | fn_ptr: on_window_visibility_changed as *mut c_void, 580 | }, 581 | NativeMethod { 582 | name: "surfaceCreatedNative".into(), 583 | sig: "(JLandroid/view/SurfaceHolder;)V".into(), 584 | fn_ptr: surface_created as *mut c_void, 585 | }, 586 | NativeMethod { 587 | name: "surfaceChangedNative".into(), 588 | sig: "(JLandroid/view/SurfaceHolder;III)V".into(), 589 | fn_ptr: surface_changed as *mut c_void, 590 | }, 591 | NativeMethod { 592 | name: "surfaceDestroyedNative".into(), 593 | sig: "(JLandroid/view/SurfaceHolder;)V".into(), 594 | fn_ptr: surface_destroyed as *mut c_void, 595 | }, 596 | NativeMethod { 597 | name: "doFrameNative".into(), 598 | sig: "(JJ)V".into(), 599 | fn_ptr: do_frame as *mut c_void, 600 | }, 601 | NativeMethod { 602 | name: "delayedCallbackNative".into(), 603 | sig: "(J)V".into(), 604 | fn_ptr: delayed_callback as *mut c_void, 605 | }, 606 | NativeMethod { 607 | name: "hasAccessibilityNodeProviderNative".into(), 608 | sig: "(J)Z".into(), 609 | fn_ptr: has_accessibility_node_provider as *mut c_void, 610 | }, 611 | NativeMethod { 612 | name: "createAccessibilityNodeInfoNative".into(), 613 | sig: "(JI)Landroid/view/accessibility/AccessibilityNodeInfo;".into(), 614 | fn_ptr: create_accessibility_node_info as *mut c_void, 615 | }, 616 | NativeMethod { 617 | name: "accessibilityFindFocusNative".into(), 618 | sig: "(JI)Landroid/view/accessibility/AccessibilityNodeInfo;".into(), 619 | fn_ptr: accessibility_find_focus as *mut c_void, 620 | }, 621 | NativeMethod { 622 | name: "performAccessibilityActionNative".into(), 623 | sig: "(JIILandroid/os/Bundle;)Z".into(), 624 | fn_ptr: perform_accessibility_action as *mut c_void, 625 | }, 626 | NativeMethod { 627 | name: "onCreateInputConnectionNative".into(), 628 | sig: "(JLandroid/view/inputmethod/EditorInfo;)Z".into(), 629 | fn_ptr: on_create_input_connection as *mut c_void, 630 | }, 631 | NativeMethod { 632 | name: "getTextBeforeCursorNative".into(), 633 | sig: "(JI)Ljava/lang/String;".into(), 634 | fn_ptr: get_text_before_cursor as *mut c_void, 635 | }, 636 | NativeMethod { 637 | name: "getTextAfterCursorNative".into(), 638 | sig: "(JI)Ljava/lang/String;".into(), 639 | fn_ptr: get_text_after_cursor as *mut c_void, 640 | }, 641 | NativeMethod { 642 | name: "getSelectedTextNative".into(), 643 | sig: "(J)Ljava/lang/String;".into(), 644 | fn_ptr: get_selected_text as *mut c_void, 645 | }, 646 | NativeMethod { 647 | name: "getCursorCapsModeNative".into(), 648 | sig: "(JI)I".into(), 649 | fn_ptr: get_cursor_caps_mode as *mut c_void, 650 | }, 651 | NativeMethod { 652 | name: "deleteSurroundingTextNative".into(), 653 | sig: "(JII)Z".into(), 654 | fn_ptr: delete_surrounding_text as *mut c_void, 655 | }, 656 | NativeMethod { 657 | name: "deleteSurroundingTextInCodePointsNative".into(), 658 | sig: "(JII)Z".into(), 659 | fn_ptr: delete_surrounding_text_in_code_points as *mut c_void, 660 | }, 661 | NativeMethod { 662 | name: "setComposingTextNative".into(), 663 | sig: "(JLjava/lang/String;I)Z".into(), 664 | fn_ptr: set_composing_text as *mut c_void, 665 | }, 666 | NativeMethod { 667 | name: "setComposingRegionNative".into(), 668 | sig: "(JII)Z".into(), 669 | fn_ptr: set_composing_region as *mut c_void, 670 | }, 671 | NativeMethod { 672 | name: "finishComposingTextNative".into(), 673 | sig: "(J)Z".into(), 674 | fn_ptr: finish_composing_text as *mut c_void, 675 | }, 676 | NativeMethod { 677 | name: "commitTextNative".into(), 678 | sig: "(JLjava/lang/String;I)Z".into(), 679 | fn_ptr: commit_text as *mut c_void, 680 | }, 681 | NativeMethod { 682 | name: "setSelectionNative".into(), 683 | sig: "(JII)Z".into(), 684 | fn_ptr: set_selection as *mut c_void, 685 | }, 686 | NativeMethod { 687 | name: "performEditorActionNative".into(), 688 | sig: "(JI)Z".into(), 689 | fn_ptr: perform_editor_action as *mut c_void, 690 | }, 691 | NativeMethod { 692 | name: "performContextMenuActionNative".into(), 693 | sig: "(JI)Z".into(), 694 | fn_ptr: perform_context_menu_action as *mut c_void, 695 | }, 696 | NativeMethod { 697 | name: "beginBatchEditNative".into(), 698 | sig: "(J)Z".into(), 699 | fn_ptr: begin_batch_edit as *mut c_void, 700 | }, 701 | NativeMethod { 702 | name: "endBatchEditNative".into(), 703 | sig: "(J)Z".into(), 704 | fn_ptr: end_batch_edit as *mut c_void, 705 | }, 706 | NativeMethod { 707 | name: "inputConnectionSendKeyEventNative".into(), 708 | sig: "(JLandroid/view/KeyEvent;)Z".into(), 709 | fn_ptr: input_connection_send_key_event as *mut c_void, 710 | }, 711 | NativeMethod { 712 | name: "inputConnectionClearMetaKeyStatesNative".into(), 713 | sig: "(JI)Z".into(), 714 | fn_ptr: input_connection_clear_meta_key_states as *mut c_void, 715 | }, 716 | NativeMethod { 717 | name: "inputConnectionReportFullscreenModeNative".into(), 718 | sig: "(JZ)Z".into(), 719 | fn_ptr: input_connection_report_fullscreen_mode as *mut c_void, 720 | }, 721 | NativeMethod { 722 | name: "requestCursorUpdatesNative".into(), 723 | sig: "(JI)Z".into(), 724 | fn_ptr: request_cursor_updates as *mut c_void, 725 | }, 726 | NativeMethod { 727 | name: "closeInputConnectionNative".into(), 728 | sig: "(J)V".into(), 729 | fn_ptr: close_input_connection as *mut c_void, 730 | }, 731 | ], 732 | ) 733 | .unwrap(); 734 | }); 735 | env.register_native_methods( 736 | class, 737 | &[NativeMethod { 738 | name: "newViewPeer".into(), 739 | sig: "(Landroid/content/Context;)J".into(), 740 | fn_ptr: new_peer as *mut c_void, 741 | }], 742 | ) 743 | .unwrap(); 744 | } 745 | -------------------------------------------------------------------------------- /src/view_configuration.rs: -------------------------------------------------------------------------------- 1 | //! A simple representation of Android `ViewConfiguration`. 2 | 3 | use jni::{JNIEnv, errors::Error, objects::JObject}; 4 | 5 | /// A representation of Android `ViewConfiguration`. 6 | /// 7 | /// This is a plain struct, and only contains non-deprecated values 8 | /// available on API level 33 and below. 9 | #[derive(Debug, Clone, Default)] 10 | #[allow(dead_code)] 11 | pub struct ViewConfiguration { 12 | /// Milliseconds between the first tap's up event and the 13 | /// subsequent tap's down event to detect a double tap. 14 | pub double_tap_timeout: i32, 15 | /// Milliseconds for a press to become a long press. 16 | pub long_press_timeout: i32, 17 | /// Milliseconds between a tap's up event and a subsequent 18 | /// tap's down event such that the subsequent tap is counted 19 | /// part of the same multi-tap sequence. 20 | pub multi_press_timeout: i32, 21 | /// Maximum pixels between a tap and a subsequent tap 22 | /// such that the subsequent tap can be counted as part of 23 | /// the same multi-tap sequence. 24 | pub scaled_double_tap_slop: i32, 25 | /// Scaling factor for the horizontal scroll axis value during 26 | /// `MotionAction::Scroll` for the number of pixels to scroll. 27 | pub scaled_horizontal_scroll_factor: f32, 28 | /// Maximum velocity, in pixels per second, to initiate a fling. 29 | pub scaled_maximum_fling_velocity: i32, 30 | /// Minimum velocity, in pixels per second, to initiate a fling. 31 | pub scaled_minimum_fling_velocity: i32, 32 | /// Minimum distance in pixels between touches before a gesture 33 | /// can be interpreted as scaling. 34 | pub scaled_minimum_scaling_span: i32, 35 | /// Pixels a touch can travel before a touch can be interpreted 36 | /// as a paging gesture. 37 | pub scaled_paging_touch_slop: i32, 38 | /// Perpendicular size of the scroll bar in pixels. 39 | pub scaled_scroll_bar_size: i32, 40 | /// Scaling factor for the vertical scroll axis value during 41 | /// `MotionAction::Scroll` for the number of pixels to scroll. 42 | pub scaled_vertical_scroll_factor: f32, 43 | /// Millisecond duration of the fade for inactive scrollbars. 44 | pub scroll_bar_fade_duration: i32, 45 | /// Milliseconds before inactive scrollbars begin to fade out. 46 | pub scroll_default_delay: i32, 47 | /// Friction factor for fling scrolls. 48 | pub scroll_friction: f32, 49 | /// Milliseconds to wait before deciding if a stationary touch 50 | /// is for a tap or for a tap. 51 | pub tap_timeout: i32, 52 | /// `true` when menus should display keyboard shortcut hints. 53 | pub should_show_menu_shortcuts_when_keyboard_present: bool, 54 | } 55 | 56 | impl ViewConfiguration { 57 | pub fn new<'local>(view: &JObject<'local>, env: &mut JNIEnv<'local>) -> Self { 58 | Self::try_new(view, env).unwrap_or_default() 59 | } 60 | 61 | fn try_new<'local>(view: &JObject<'local>, env: &mut JNIEnv<'local>) -> Result { 62 | const CL: &str = "android/view/ViewConfiguration"; 63 | 64 | let context = env 65 | .call_method(view, "getContext", "()Landroid/content/Context;", &[])? 66 | .l()?; 67 | 68 | let vc = env 69 | .call_static_method( 70 | CL, 71 | "get", 72 | "(Landroid/content/Context;)Landroid/view/ViewConfiguration;", 73 | &[(&context).into()], 74 | )? 75 | .l()?; 76 | 77 | Ok(Self { 78 | double_tap_timeout: env 79 | .call_static_method(CL, "getDoubleTapTimeout", "()I", &[])? 80 | .i()?, 81 | long_press_timeout: env 82 | .call_static_method(CL, "getLongPressTimeout", "()I", &[])? 83 | .i()?, 84 | multi_press_timeout: env 85 | .call_static_method(CL, "getMultiPressTimeout", "()I", &[])? 86 | .i()?, 87 | scaled_double_tap_slop: env 88 | .call_method(&vc, "getScaledDoubleTapSlop", "()I", &[])? 89 | .i()?, 90 | scaled_horizontal_scroll_factor: env 91 | .call_method(&vc, "getScaledHorizontalScrollFactor", "()F", &[])? 92 | .f()?, 93 | scaled_maximum_fling_velocity: env 94 | .call_method(&vc, "getScaledMaximumFlingVelocity", "()I", &[])? 95 | .i()?, 96 | scaled_minimum_fling_velocity: env 97 | .call_method(&vc, "getScaledMinimumFlingVelocity", "()I", &[])? 98 | .i()?, 99 | scaled_minimum_scaling_span: env 100 | .call_method(&vc, "getScaledMinimumScalingSpan", "()I", &[])? 101 | .i()?, 102 | scaled_paging_touch_slop: env 103 | .call_method(&vc, "getScaledPagingTouchSlop", "()I", &[])? 104 | .i()?, 105 | scaled_scroll_bar_size: env 106 | .call_method(&vc, "getScaledScrollBarSize", "()I", &[])? 107 | .i()?, 108 | scaled_vertical_scroll_factor: env 109 | .call_method(&vc, "getScaledVerticalScrollFactor", "()F", &[])? 110 | .f()?, 111 | scroll_bar_fade_duration: env 112 | .call_static_method(CL, "getScrollBarFadeDuration", "()I", &[])? 113 | .i()?, 114 | scroll_default_delay: env 115 | .call_static_method(CL, "getScrollDefaultDelay", "()I", &[])? 116 | .i()?, 117 | scroll_friction: env 118 | .call_static_method(CL, "getScrollFriction", "()F", &[])? 119 | .f()?, 120 | tap_timeout: env 121 | .call_static_method(CL, "getTapTimeout", "()I", &[])? 122 | .i()?, 123 | should_show_menu_shortcuts_when_keyboard_present: env 124 | .call_method( 125 | &vc, 126 | "shouldShowMenuShortcutsWhenKeyboardPresent", 127 | "()Z", 128 | &[], 129 | )? 130 | .z()?, 131 | }) 132 | } 133 | } 134 | --------------------------------------------------------------------------------