├── .gitignore
├── GameActivity
├── Cargo.toml
├── README.md
├── app
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ └── co
│ │ │ └── realfit
│ │ │ └── example
│ │ │ └── MainActivity.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
│ │ ├── colors.xml
│ │ └── themes.xml
├── build.gradle
├── build.sh
├── gradle.properties
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
│ ├── clipboard.rs
│ ├── controls.rs
│ ├── frag.wgsl
│ ├── java.rs
│ ├── lib.rs
│ ├── scene.rs
│ └── vert.wgsl
├── LICENSE
├── NativeActivity
├── Cargo.toml
├── README.md
├── app
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ └── co
│ │ │ └── realfit
│ │ │ └── example
│ │ │ └── MainActivity.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
├── build.gradle
├── build.sh
├── gradle.properties
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
│ ├── clipboard.rs
│ ├── controls.rs
│ ├── frag.wgsl
│ ├── java.rs
│ ├── lib.rs
│ ├── scene.rs
│ └── vert.wgsl
├── README.md
├── pixel_1.png
├── pixel_2.png
├── pixel_3.png
├── watch_1.png
├── watch_2.png
└── watch_3.png
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | local.properties
4 | .DS_Store
5 | build
6 | captures
7 | .externalNativeBuild
8 | .cxx
9 | *.so
10 |
11 | .idea
12 | jniLibs
13 | Cargo.lock
14 | target
15 |
--------------------------------------------------------------------------------
/GameActivity/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "android-iced-example"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [lib]
7 | name = "example"
8 | crate-type = ["cdylib"]
9 |
10 | [dependencies]
11 | log = "0.4"
12 | android_logger = "0.14.1"
13 | android-activity = { version = "0.6", features = ["game-activity"] }
14 | ndk-context = "0.1"
15 | jni = { version = "0.21", features = ["invocation"] }
16 | # ndk-sys = "0.6.0"
17 | # ndk = "0.9.0"
18 | futures = "0.3"
19 |
20 | [dependencies.iced_core]
21 | git = "https://github.com/ibaryshnikov/iced.git"
22 | rev = "009bf6c"
23 | # path = "../../iced/core"
24 |
25 | [dependencies.iced_widget]
26 | git = "https://github.com/ibaryshnikov/iced.git"
27 | rev = "009bf6c"
28 | # path = "../../iced/widget"
29 | features = ["wgpu"]
30 |
31 | [dependencies.iced_winit]
32 | git = "https://github.com/ibaryshnikov/iced.git"
33 | rev = "009bf6c"
34 | # path = "../../iced/winit"
35 |
36 | [dependencies.iced_wgpu]
37 | git = "https://github.com/ibaryshnikov/iced.git"
38 | rev = "009bf6c"
39 | # path = "../../iced/wgpu"
40 |
41 | [patch.crates-io]
42 | softbuffer = { git = "https://github.com/MarijnS95/softbuffer.git", rev = "d5cc95a" } # branch = "android"
43 | # android-activity = { path = "../../android-activity/android-activity" }
44 |
--------------------------------------------------------------------------------
/GameActivity/README.md:
--------------------------------------------------------------------------------
1 | # Example of building android app with iced
2 |
3 | This is a `GameActivity` example, based on `agdk-mainloop` from
4 | [android-activity](https://github.com/rust-mobile/android-activity)
5 |
6 | **Important:** there's an [issue](https://github.com/rust-mobile/android-activity/issues/79)
7 | with event filters in emulator. To use touch screen in emulator,
8 | you'll have to clone `android-activity` and change the default filter.
9 |
10 |
11 | ## Building and running
12 |
13 | Check `android-activity` crate for detailed instructions.
14 | During my tests I was running the following command and using android studio afterwards:
15 |
16 | ```bash
17 | export ANDROID_NDK_HOME="path/to/ndk"
18 | export ANDROID_HOME="path/to/sdk"
19 |
20 | rustup target add x86_64-linux-android
21 | cargo install cargo-ndk
22 |
23 | cargo ndk -t x86_64 -o app/src/main/jniLibs/ build
24 | ```
25 |
--------------------------------------------------------------------------------
/GameActivity/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/GameActivity/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | }
4 |
5 | android {
6 | ndkVersion "25.2.9519653"
7 | compileSdk 35
8 |
9 | defaultConfig {
10 | applicationId "co.realfit.example"
11 | minSdk 28
12 | targetSdk 35
13 | versionCode 1
14 | versionName "1.0"
15 | }
16 |
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
21 | }
22 | debug {
23 | minifyEnabled false
24 | //packagingOptions {
25 | // doNotStrip '**/*.so'
26 | //}
27 | //debuggable true
28 | }
29 | }
30 | compileOptions {
31 | sourceCompatibility JavaVersion.VERSION_1_8
32 | targetCompatibility JavaVersion.VERSION_1_8
33 | }
34 | namespace 'co.realfit.example'
35 | }
36 |
37 | dependencies {
38 |
39 | implementation "androidx.core:core:1.5.0"
40 | implementation "androidx.constraintlayout:constraintlayout:2.0.4"
41 | implementation 'androidx.fragment:fragment:1.2.5'
42 | implementation 'com.google.oboe:oboe:1.5.0'
43 |
44 | // To use the Android Frame Pacing library
45 | //implementation "androidx.games:games-frame-pacing:1.9.1"
46 |
47 | // To use the Android Performance Tuner
48 | //implementation "androidx.games:games-performance-tuner:1.5.0"
49 |
50 | // To use the Games Activity library
51 | implementation "androidx.games:games-activity:2.0.2"
52 |
53 | // To use the Games Controller Library
54 | //implementation "androidx.games:games-controller:2.0.2"
55 |
56 | // To use the Games Text Input Library
57 | //implementation "androidx.games:games-text-input:2.0.2"
58 | }
59 |
60 |
--------------------------------------------------------------------------------
/GameActivity/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
--------------------------------------------------------------------------------
/GameActivity/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/GameActivity/app/src/main/java/co/realfit/example/MainActivity.java:
--------------------------------------------------------------------------------
1 | package co.realfit.example;
2 |
3 | import android.content.ClipData;
4 | import android.content.ClipboardManager;
5 | import android.content.Context;
6 | import androidx.core.view.WindowCompat;
7 | import androidx.core.view.WindowInsetsCompat;
8 | import androidx.core.view.WindowInsetsControllerCompat;
9 |
10 | import com.google.androidgamesdk.GameActivity;
11 |
12 | import android.os.Bundle;
13 | import android.util.Log;
14 | import android.view.inputmethod.InputMethodManager;
15 | import android.content.pm.PackageManager;
16 | import android.os.Build.VERSION;
17 | import android.os.Build.VERSION_CODES;
18 | import android.view.View;
19 | import android.view.WindowManager;
20 |
21 | public class MainActivity extends GameActivity {
22 |
23 | static {
24 | // Load the STL first to workaround issues on old Android versions:
25 | // "if your app targets a version of Android earlier than Android 4.3
26 | // (Android API level 18),
27 | // and you use libc++_shared.so, you must load the shared library before any other
28 | // library that depends on it."
29 | // See https://developer.android.com/ndk/guides/cpp-support#shared_runtimes
30 | //System.loadLibrary("c++_shared");
31 |
32 | // Load the native library.
33 | // The name "android-game" depends on your CMake configuration, must be
34 | // consistent here and inside AndroidManifect.xml
35 | System.loadLibrary("example");
36 | }
37 |
38 | private void hideSystemUI() {
39 | // This will put the game behind any cutouts and waterfalls on devices which have
40 | // them, so the corresponding insets will be non-zero.
41 | if (VERSION.SDK_INT >= VERSION_CODES.P) {
42 | getWindow().getAttributes().layoutInDisplayCutoutMode
43 | = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
44 | }
45 | // From API 30 onwards, this is the recommended way to hide the system UI, rather than
46 | // using View.setSystemUiVisibility.
47 | View decorView = getWindow().getDecorView();
48 | WindowInsetsControllerCompat controller = new WindowInsetsControllerCompat(getWindow(),
49 | decorView);
50 | controller.hide(WindowInsetsCompat.Type.systemBars());
51 | controller.hide(WindowInsetsCompat.Type.displayCutout());
52 | controller.setSystemBarsBehavior(
53 | WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
54 | }
55 |
56 | @Override
57 | protected void onCreate(Bundle savedInstanceState) {
58 | // When true, the app will fit inside any system UI windows.
59 | // When false, we render behind any system UI windows.
60 | WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
61 | hideSystemUI();
62 | // You can set IME fields here or in native code using GameActivity_setImeEditorInfoFields.
63 | // We set the fields in native_engine.cpp.
64 | // super.setImeEditorInfoFields(InputType.TYPE_CLASS_TEXT,
65 | // IME_ACTION_NONE, IME_FLAG_NO_FULLSCREEN );
66 | super.onCreate(savedInstanceState);
67 | }
68 |
69 | protected void onResume() {
70 | super.onResume();
71 | hideSystemUI();
72 | }
73 |
74 | public boolean isGooglePlayGames() {
75 | PackageManager pm = getPackageManager();
76 | return pm.hasSystemFeature("com.google.android.play.feature.HPE_EXPERIENCE");
77 | }
78 |
79 | private void showKeyboard() {
80 | Log.d("MainActivity", "showKeyboard instance method called");
81 | InputMethodManager inputManager = getSystemService(InputMethodManager.class);
82 | inputManager.showSoftInput(getWindow().getDecorView(), InputMethodManager.SHOW_IMPLICIT);
83 | }
84 |
85 | private void hideKeyboard() {
86 | Log.d("MainActivity", "hideKeyboard instance method called");
87 | InputMethodManager inputManager = getSystemService(InputMethodManager.class);
88 | inputManager.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0);
89 | }
90 |
91 | private String readClipboard() {
92 | ClipboardManager clipboardManager = (ClipboardManager) getApplicationContext().getSystemService(Context.CLIPBOARD_SERVICE);
93 | ClipData data = clipboardManager.getPrimaryClip();
94 | if (data == null) {
95 | Log.d("MainActivity", "ClipData in readClipboard is null");
96 | return "";
97 | }
98 | ClipData.Item item = data.getItemAt(0);
99 | if (item == null) {
100 | Log.d("MainActivity", "Item in readClipboard is null");
101 | return "";
102 | }
103 | return item.coerceToText(this).toString();
104 | }
105 |
106 | private void writeClipboard(String value) {
107 | ClipboardManager clipboardManager = (ClipboardManager) getApplicationContext().getSystemService(Context.CLIPBOARD_SERVICE);
108 | ClipData data = ClipData.newPlainText("MainActivity text", value);
109 | clipboardManager.setPrimaryClip(data);
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/GameActivity/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/GameActivity/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 |
--------------------------------------------------------------------------------
/GameActivity/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/GameActivity/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/GameActivity/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/GameActivity/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/GameActivity/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/GameActivity/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/GameActivity/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/GameActivity/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/GameActivity/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/GameActivity/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/GameActivity/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/GameActivity/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/GameActivity/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/GameActivity/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/GameActivity/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/GameActivity/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/GameActivity/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/GameActivity/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/GameActivity/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/GameActivity/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/GameActivity/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/GameActivity/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/GameActivity/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/GameActivity/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/GameActivity/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.8.0' apply false
4 | id 'com.android.library' version '8.8.0' apply false
5 | }
6 |
7 | tasks.register('clean', Delete) {
8 | delete rootProject.layout.buildDirectory
9 | }
10 |
--------------------------------------------------------------------------------
/GameActivity/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | cargo ndk -t x86_64 -o app/src/main/jniLibs/ build
4 |
--------------------------------------------------------------------------------
/GameActivity/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
--------------------------------------------------------------------------------
/GameActivity/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/GameActivity/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/GameActivity/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/GameActivity/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original 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 POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
88 |
89 | # Use the maximum available, or set MAX_FD != -1 to use that value.
90 | MAX_FD=maximum
91 |
92 | warn () {
93 | echo "$*"
94 | } >&2
95 |
96 | die () {
97 | echo
98 | echo "$*"
99 | echo
100 | exit 1
101 | } >&2
102 |
103 | # OS specific support (must be 'true' or 'false').
104 | cygwin=false
105 | msys=false
106 | darwin=false
107 | nonstop=false
108 | case "$( uname )" in #(
109 | CYGWIN* ) cygwin=true ;; #(
110 | Darwin* ) darwin=true ;; #(
111 | MSYS* | MINGW* ) msys=true ;; #(
112 | NONSTOP* ) nonstop=true ;;
113 | esac
114 |
115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
116 |
117 |
118 | # Determine the Java command to use to start the JVM.
119 | if [ -n "$JAVA_HOME" ] ; then
120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
121 | # IBM's JDK on AIX uses strange locations for the executables
122 | JAVACMD=$JAVA_HOME/jre/sh/java
123 | else
124 | JAVACMD=$JAVA_HOME/bin/java
125 | fi
126 | if [ ! -x "$JAVACMD" ] ; then
127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
128 |
129 | Please set the JAVA_HOME variable in your environment to match the
130 | location of your Java installation."
131 | fi
132 | else
133 | JAVACMD=java
134 | if ! command -v java >/dev/null 2>&1
135 | then
136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 | fi
142 |
143 | # Increase the maximum file descriptors if we can.
144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
145 | case $MAX_FD in #(
146 | max*)
147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
148 | # shellcheck disable=SC2039,SC3045
149 | MAX_FD=$( ulimit -H -n ) ||
150 | warn "Could not query maximum file descriptor limit"
151 | esac
152 | case $MAX_FD in #(
153 | '' | soft) :;; #(
154 | *)
155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
156 | # shellcheck disable=SC2039,SC3045
157 | ulimit -n "$MAX_FD" ||
158 | warn "Could not set maximum file descriptor limit to $MAX_FD"
159 | esac
160 | fi
161 |
162 | # Collect all arguments for the java command, stacking in reverse order:
163 | # * args from the command line
164 | # * the main class name
165 | # * -classpath
166 | # * -D...appname settings
167 | # * --module-path (only if needed)
168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
169 |
170 | # For Cygwin or MSYS, switch paths to Windows format before running java
171 | if "$cygwin" || "$msys" ; then
172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
174 |
175 | JAVACMD=$( cygpath --unix "$JAVACMD" )
176 |
177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
178 | for arg do
179 | if
180 | case $arg in #(
181 | -*) false ;; # don't mess with options #(
182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
183 | [ -e "$t" ] ;; #(
184 | *) false ;;
185 | esac
186 | then
187 | arg=$( cygpath --path --ignore --mixed "$arg" )
188 | fi
189 | # Roll the args list around exactly as many times as the number of
190 | # args, so each arg winds up back in the position where it started, but
191 | # possibly modified.
192 | #
193 | # NB: a `for` loop captures its iteration list before it begins, so
194 | # changing the positional parameters here affects neither the number of
195 | # iterations, nor the values presented in `arg`.
196 | shift # remove old arg
197 | set -- "$@" "$arg" # push replacement arg
198 | done
199 | fi
200 |
201 |
202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
204 |
205 | # Collect all arguments for the java command:
206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
207 | # and any embedded shellness will be escaped.
208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
209 | # treated as '${Hostname}' itself on the command line.
210 |
211 | set -- \
212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
213 | -classpath "$CLASSPATH" \
214 | org.gradle.wrapper.GradleWrapperMain \
215 | "$@"
216 |
217 | # Stop when "xargs" is not available.
218 | if ! command -v xargs >/dev/null 2>&1
219 | then
220 | die "xargs is not available"
221 | fi
222 |
223 | # Use "xargs" to parse quoted args.
224 | #
225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
226 | #
227 | # In Bash we could simply go:
228 | #
229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
230 | # set -- "${ARGS[@]}" "$@"
231 | #
232 | # but POSIX shell has neither arrays nor command substitution, so instead we
233 | # post-process each arg (as a line of input to sed) to backslash-escape any
234 | # character that might be a shell metacharacter, then use eval to reverse
235 | # that process (while maintaining the separation between arguments), and wrap
236 | # the whole thing up as a single "set" statement.
237 | #
238 | # This will of course break if any of these variables contains a newline or
239 | # an unmatched quote.
240 | #
241 |
242 | eval "set -- $(
243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
244 | xargs -n1 |
245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
246 | tr '\n' ' '
247 | )" '"$@"'
248 |
249 | exec "$JAVACMD" "$@"
250 |
--------------------------------------------------------------------------------
/GameActivity/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 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/GameActivity/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 | //rootProject.name = "Rust Template"
16 | include ':app'
17 |
--------------------------------------------------------------------------------
/GameActivity/src/clipboard.rs:
--------------------------------------------------------------------------------
1 | use jni::objects::{JObject, JString, JValue};
2 |
3 | use crate::java::{get_env, get_vm};
4 |
5 | pub(crate) struct Clipboard {}
6 |
7 | impl iced_core::Clipboard for Clipboard {
8 | fn read(&self, _kind: iced_core::clipboard::Kind) -> Option {
9 | log::debug!("Clipboard read method called");
10 | match read_clipboard() {
11 | Ok(text) => Some(text),
12 | Err(e) => {
13 | log::error!("Error reading from clipboard: {e}");
14 | None
15 | }
16 | }
17 | }
18 | fn write(&mut self, _kind: iced_core::clipboard::Kind, contents: String) {
19 | log::debug!("Clipboard write method called");
20 | if let Err(e) = write_clipboard(contents) {
21 | log::error!("Error writing to clipboard: {e}");
22 | }
23 | }
24 | }
25 |
26 | fn read_clipboard() -> jni::errors::Result {
27 | let ctx = ndk_context::android_context();
28 | let vm = get_vm(&ctx);
29 | let mut env = get_env(&vm);
30 | let activity = unsafe { JObject::from_raw(ctx.context() as _) };
31 | let j_object = env
32 | .call_method(activity, "readClipboard", "()Ljava/lang/String;", &[])?
33 | .l()?;
34 | let j_string = JString::from(j_object);
35 | env.get_string(&j_string).map(Into::into)
36 | }
37 | fn write_clipboard(contents: String) -> jni::errors::Result<()> {
38 | let ctx = ndk_context::android_context();
39 | let vm = get_vm(&ctx);
40 | let mut env = get_env(&vm);
41 | let activity = unsafe { JObject::from_raw(ctx.context() as _) };
42 | let value = env.new_string(contents)?;
43 | let args = [JValue::Object(value.as_ref())];
44 | env.call_method(activity, "writeClipboard", "(Ljava/lang/String;)V", &args)?;
45 | Ok(())
46 | }
47 |
--------------------------------------------------------------------------------
/GameActivity/src/controls.rs:
--------------------------------------------------------------------------------
1 | use iced_wgpu::Renderer;
2 | use iced_widget::{
3 | button, column, container, horizontal_space, pick_list, row, slider, text, text_editor,
4 | text_input, vertical_space, PickList, Slider, Space,
5 | };
6 | use iced_winit::core::{Alignment, Color, Element, Length, Theme};
7 | use iced_winit::runtime::{Program, Task};
8 | use iced_winit::winit::event_loop::EventLoopProxy;
9 |
10 | use crate::UserEvent;
11 |
12 | const EXAMPLES: [Example; 3] = [Example::Integration, Example::Counter, Example::TextEditor];
13 |
14 | #[derive(Debug, Clone, Copy, PartialEq, Eq)]
15 | pub enum Example {
16 | Integration,
17 | Counter,
18 | TextEditor,
19 | }
20 |
21 | impl std::fmt::Display for Example {
22 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 | write!(f, "{:?}", self)
24 | }
25 | }
26 |
27 | pub struct Controls {
28 | background_color: Color,
29 | input: String,
30 | value: i32,
31 | selected_example: Example,
32 | editor: text_editor::Content,
33 | proxy: EventLoopProxy,
34 | }
35 |
36 | #[derive(Debug, Clone)]
37 | pub enum Message {
38 | RedChanged(f32),
39 | GreenChanged(f32),
40 | BlueChanged(f32),
41 | InputChanged(String),
42 | EditorAction(text_editor::Action),
43 | ExampleSelected(Example),
44 | Inc,
45 | Dec,
46 | }
47 |
48 | impl Controls {
49 | pub fn new(proxy: EventLoopProxy) -> Controls {
50 | Controls {
51 | background_color: Color::BLACK,
52 | input: String::default(),
53 | value: 0,
54 | selected_example: Example::Integration,
55 | editor: text_editor::Content::new(),
56 | proxy,
57 | }
58 | }
59 |
60 | pub fn background_color(&self) -> Color {
61 | self.background_color
62 | }
63 | }
64 |
65 | impl Program for Controls {
66 | type Theme = Theme;
67 | type Message = Message;
68 | type Renderer = Renderer;
69 |
70 | fn update(&mut self, message: Message) -> Task {
71 | match message {
72 | Message::Inc => self.value += 1,
73 | Message::Dec => self.value -= 1,
74 | Message::ExampleSelected(example) => self.selected_example = example,
75 | Message::InputChanged(value) => self.input = value,
76 | Message::RedChanged(r) => self.background_color.r = r,
77 | Message::GreenChanged(g) => self.background_color.g = g,
78 | Message::BlueChanged(b) => self.background_color.b = b,
79 | Message::EditorAction(action) => match action {
80 | text_editor::Action::Focus => {
81 | log::info!("Editor focused");
82 | // it's possible to call java::call_instance_method("showKeyboard")
83 | // right here, but needed something to show the usage of user events
84 | let _ = self.proxy.send_event(UserEvent::ShowKeyboard);
85 | }
86 | text_editor::Action::Blur => {
87 | log::info!("Editor lost focus");
88 | let _ = self.proxy.send_event(UserEvent::HideKeyboard);
89 | }
90 | other => self.editor.perform(other),
91 | },
92 | }
93 |
94 | Task::none()
95 | }
96 |
97 | fn view(&self) -> Element {
98 | match self.selected_example {
99 | Example::Integration => self.integration(),
100 | Example::Counter => self.counter(),
101 | Example::TextEditor => self.text_editor(),
102 | }
103 | }
104 | }
105 |
106 | fn color_slider<'a>(value: f32, f: impl Fn(f32) -> Message + 'a) -> Slider<'a, f32, Message> {
107 | slider(0.0..=1.0, value, f).step(0.01)
108 | }
109 |
110 | impl Controls {
111 | fn examples(&self) -> PickList {
112 | pick_list(
113 | &EXAMPLES[..],
114 | Some(self.selected_example),
115 | Message::ExampleSelected,
116 | )
117 | }
118 | fn integration(&self) -> Element {
119 | let sliders = row![
120 | color_slider(self.background_color.r, Message::RedChanged),
121 | color_slider(self.background_color.g, Message::GreenChanged),
122 | color_slider(self.background_color.b, Message::BlueChanged),
123 | ]
124 | .width(Length::Fill)
125 | .spacing(20);
126 |
127 | container(
128 | column![
129 | Space::with_height(20),
130 | self.examples(),
131 | vertical_space(),
132 | row![
133 | text!("{:?}", self.background_color).size(14),
134 | horizontal_space(),
135 | ],
136 | text_input("Placeholder", &self.input).on_input(Message::InputChanged),
137 | sliders,
138 | Space::with_height(20),
139 | ]
140 | .align_x(Alignment::Center)
141 | .spacing(10),
142 | )
143 | .padding(10)
144 | .into()
145 | }
146 |
147 | fn counter(&self) -> Element {
148 | container(
149 | column![
150 | Space::with_height(30),
151 | self.examples(),
152 | vertical_space(),
153 | button("Increment").on_press(Message::Inc),
154 | text!("{}", self.value).size(50),
155 | button("Decrement").on_press(Message::Dec),
156 | vertical_space(),
157 | Space::with_height(100),
158 | ]
159 | .align_x(Alignment::Center)
160 | .spacing(10),
161 | )
162 | .center(Length::Fill)
163 | .style(add_background)
164 | .into()
165 | }
166 |
167 | fn text_editor(&self) -> Element {
168 | container(
169 | column![
170 | Space::with_height(30),
171 | self.examples(),
172 | vertical_space(),
173 | text_editor::(&self.editor)
174 | .height(400)
175 | .on_action(Message::EditorAction),
176 | vertical_space(),
177 | ]
178 | .align_x(Alignment::Center),
179 | )
180 | .padding(10)
181 | .center(Length::Fill)
182 | .style(add_background)
183 | .into()
184 | }
185 | }
186 |
187 | fn add_background(theme: &Theme) -> container::Style {
188 | theme.palette().background.into()
189 | }
190 |
--------------------------------------------------------------------------------
/GameActivity/src/frag.wgsl:
--------------------------------------------------------------------------------
1 | @fragment
2 | fn main() -> @location(0) vec4 {
3 | return vec4(1.0, 0.0, 0.0, 1.0);
4 | }
5 |
--------------------------------------------------------------------------------
/GameActivity/src/java.rs:
--------------------------------------------------------------------------------
1 | use jni::objects::JObject;
2 | use jni::{AttachGuard, JavaVM};
3 |
4 | //
5 | // some jni syntax hints
6 | //
7 | // L - class, for example: Ljava/lang/String;
8 | // primitives: Z - boolean, I - integer, V - void
9 | // for example, Rust signature fn get_text(flag: bool) -> String
10 | // will become "(Z)Ljava/lang/String;"
11 | //
12 | // in find_class . is replaced by $
13 | // docs: android/view/WindowManager.LayoutParams
14 | // jni: android/view/WindowManager$LayoutParams
15 | // it also has some quirks:
16 | // https://developer.android.com/training/articles/perf-jni.html#faq_FindClass
17 | //
18 |
19 | pub(crate) fn call_instance_method(name: &str) {
20 | log::debug!("Calling instance method from Rust: {}", name);
21 | let ctx = ndk_context::android_context();
22 | let vm = get_vm(&ctx);
23 | let mut env = get_env(&vm);
24 | let activity = unsafe { JObject::from_raw(ctx.context() as _) };
25 | if let Err(e) = env.call_method(activity, name, "()V", &[]) {
26 | log::error!("Error calling instance method {}: {}", name, e);
27 | }
28 | }
29 |
30 | pub(crate) fn get_vm(ctx: &ndk_context::AndroidContext) -> JavaVM {
31 | unsafe { JavaVM::from_raw(ctx.vm() as _) }.unwrap_or_else(|e| {
32 | log::error!("Error getting ctx.vm(): {:?}", e);
33 | panic!("No JavaVM found");
34 | })
35 | }
36 |
37 | pub(crate) fn get_env(vm: &JavaVM) -> AttachGuard {
38 | vm.attach_current_thread().unwrap_or_else(|e| {
39 | log::error!("Error attaching vm: {:?}", e);
40 | panic!("Failed to call attach_current_thread for JavaVM");
41 | })
42 | }
43 |
--------------------------------------------------------------------------------
/GameActivity/src/lib.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Arc;
2 |
3 | use iced_wgpu::graphics::Viewport;
4 | use iced_wgpu::{wgpu, Engine, Renderer};
5 | use iced_winit::core::{mouse, renderer, Font, Pixels, Size, Theme};
6 | use iced_winit::runtime::{program, Debug};
7 | use iced_winit::{conversion, winit};
8 | use log::LevelFilter;
9 | use wgpu::{Device, Instance, Queue, TextureFormat};
10 | use winit::application::ApplicationHandler;
11 | use winit::event::{DeviceEvent, DeviceId, ElementState, StartCause, WindowEvent};
12 | use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy};
13 | use winit::keyboard::{KeyCode, ModifiersState, PhysicalKey};
14 | use winit::platform::android::activity::AndroidApp;
15 | use winit::platform::android::EventLoopBuilderExtAndroid;
16 | use winit::window::{Window, WindowId};
17 |
18 | mod clipboard;
19 | mod controls;
20 | mod java;
21 | mod scene;
22 |
23 | use clipboard::Clipboard;
24 | use controls::Controls;
25 | use scene::Scene;
26 |
27 | // winit ime support
28 | // https://github.com/rust-windowing/winit/pull/2993
29 |
30 | // issue with android-activity crate default_motion_filter function
31 | // https://github.com/rust-mobile/android-activity/issues/79
32 |
33 | #[no_mangle]
34 | fn android_main(android_app: AndroidApp) {
35 | let logger_config = android_logger::Config::default().with_max_level(LevelFilter::Info);
36 | android_logger::init_once(logger_config);
37 |
38 | log::info!("android_main started");
39 |
40 | let event_loop = EventLoop::with_user_event()
41 | .with_android_app(android_app)
42 | .build()
43 | .expect("Should build event loop");
44 |
45 | let proxy = event_loop.create_proxy();
46 |
47 | let mut app = App::new(proxy);
48 | event_loop.run_app(&mut app).expect("Should run event loop");
49 | }
50 |
51 | #[derive(Debug)]
52 | enum UserEvent {
53 | ShowKeyboard,
54 | HideKeyboard,
55 | }
56 |
57 | struct App {
58 | proxy: EventLoopProxy,
59 | app_data: Option,
60 | resized: bool,
61 | cursor_position: Option>,
62 | modifiers: ModifiersState,
63 | }
64 |
65 | struct AppData {
66 | state: program::State,
67 | scene: Scene,
68 | window: Arc,
69 | device: Device,
70 | queue: Queue,
71 | surface: wgpu::Surface<'static>,
72 | format: TextureFormat,
73 | engine: Engine,
74 | renderer: Renderer,
75 | clipboard: Clipboard,
76 | viewport: Viewport,
77 | debug: Debug,
78 | }
79 |
80 | impl App {
81 | fn new(proxy: EventLoopProxy) -> Self {
82 | Self {
83 | proxy,
84 | app_data: None,
85 | resized: false,
86 | cursor_position: None,
87 | modifiers: ModifiersState::default(),
88 | }
89 | }
90 | }
91 |
92 | impl ApplicationHandler for App {
93 | fn new_events(&mut self, _event_loop: &ActiveEventLoop, _cause: StartCause) {
94 | // log::info!("New events cause {:?}", cause);
95 | }
96 |
97 | fn resumed(&mut self, event_loop: &ActiveEventLoop) {
98 | log::info!("Resumed");
99 | // if self.app_data.is_some() {
100 | // log::info!("Already initialized, skipping");
101 | // return;
102 | // }
103 |
104 | let instance = Instance::new(wgpu::InstanceDescriptor {
105 | backends: wgpu::Backends::all(),
106 | ..Default::default()
107 | });
108 |
109 | let attrs = Window::default_attributes();
110 | let window = Arc::new(event_loop.create_window(attrs).unwrap());
111 | window.set_ime_allowed(true);
112 |
113 | let physical_size = window.inner_size();
114 | let viewport = Viewport::with_physical_size(
115 | Size::new(physical_size.width, physical_size.height),
116 | window.scale_factor(),
117 | );
118 | let clipboard = Clipboard {};
119 |
120 | let surface = instance
121 | .create_surface(window.clone())
122 | .expect("Create window surface");
123 |
124 | let (format, adapter, device, queue) = futures::executor::block_on(async {
125 | let adapter =
126 | wgpu::util::initialize_adapter_from_env_or_default(&instance, Some(&surface))
127 | .await
128 | .expect("Create adapter");
129 |
130 | let adapter_features = adapter.features();
131 |
132 | let capabilities = surface.get_capabilities(&adapter);
133 |
134 | let (device, queue) = adapter
135 | .request_device(
136 | &wgpu::DeviceDescriptor {
137 | label: None,
138 | required_features: adapter_features & wgpu::Features::default(),
139 | required_limits: wgpu::Limits::default(),
140 | memory_hints: wgpu::MemoryHints::MemoryUsage,
141 | },
142 | None,
143 | )
144 | .await
145 | .expect("Request device");
146 |
147 | (
148 | capabilities
149 | .formats
150 | .iter()
151 | .copied()
152 | .find(wgpu::TextureFormat::is_srgb)
153 | .or_else(|| capabilities.formats.first().copied())
154 | .expect("Get preferred format"),
155 | adapter,
156 | device,
157 | queue,
158 | )
159 | });
160 |
161 | surface.configure(
162 | &device,
163 | &wgpu::SurfaceConfiguration {
164 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
165 | format,
166 | width: physical_size.width,
167 | height: physical_size.height,
168 | present_mode: wgpu::PresentMode::AutoVsync,
169 | alpha_mode: wgpu::CompositeAlphaMode::Auto,
170 | view_formats: vec![],
171 | desired_maximum_frame_latency: 2,
172 | },
173 | );
174 |
175 | let scene = Scene::new(&device, format);
176 | let controls = Controls::new(self.proxy.clone());
177 |
178 | let mut debug = Debug::new();
179 | let engine = Engine::new(&adapter, &device, &queue, format, None);
180 | let mut renderer = Renderer::new(&device, &engine, Font::default(), Pixels::from(16));
181 |
182 | let state =
183 | program::State::new(controls, viewport.logical_size(), &mut renderer, &mut debug);
184 |
185 | event_loop.set_control_flow(ControlFlow::Wait);
186 |
187 | self.cursor_position = None;
188 | self.modifiers = ModifiersState::default();
189 |
190 | let app_data = AppData {
191 | state,
192 | scene,
193 | window,
194 | device,
195 | queue,
196 | surface,
197 | format,
198 | engine,
199 | renderer,
200 | clipboard,
201 | viewport,
202 | debug,
203 | };
204 | self.app_data = Some(app_data);
205 | }
206 |
207 | fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) {
208 | match event {
209 | UserEvent::ShowKeyboard => {
210 | java::call_instance_method("showKeyboard");
211 | }
212 | UserEvent::HideKeyboard => {
213 | java::call_instance_method("hideKeyboard");
214 | }
215 | }
216 | }
217 |
218 | fn device_event(
219 | &mut self,
220 | _event_loop: &ActiveEventLoop,
221 | _device_id: DeviceId,
222 | event: DeviceEvent,
223 | ) {
224 | log::info!("DeviceEvent {:?}", event);
225 | }
226 |
227 | fn window_event(
228 | &mut self,
229 | event_loop: &ActiveEventLoop,
230 | _window_id: WindowId,
231 | event: WindowEvent,
232 | ) {
233 | log::info!("Window event: {:?}", event);
234 |
235 | let Some(app_data) = self.app_data.as_mut() else {
236 | return;
237 | };
238 |
239 | let AppData {
240 | state,
241 | scene,
242 | window,
243 | device,
244 | queue,
245 | surface,
246 | format,
247 | engine,
248 | renderer,
249 | clipboard,
250 | debug,
251 | ..
252 | } = app_data;
253 |
254 | match event {
255 | WindowEvent::CloseRequested => {
256 | event_loop.exit();
257 | }
258 | WindowEvent::RedrawRequested => {
259 | if self.resized {
260 | let size = window.inner_size();
261 |
262 | app_data.viewport = Viewport::with_physical_size(
263 | Size::new(size.width, size.height),
264 | window.scale_factor(),
265 | );
266 |
267 | surface.configure(
268 | device,
269 | &wgpu::SurfaceConfiguration {
270 | format: *format,
271 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
272 | width: size.width,
273 | height: size.height,
274 | present_mode: wgpu::PresentMode::AutoVsync,
275 | alpha_mode: wgpu::CompositeAlphaMode::Auto,
276 | view_formats: vec![],
277 | desired_maximum_frame_latency: 2,
278 | },
279 | );
280 |
281 | self.resized = false;
282 | }
283 |
284 | match surface.get_current_texture() {
285 | Ok(frame) => {
286 | let mut encoder =
287 | device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
288 | label: None,
289 | });
290 |
291 | let program = state.program();
292 |
293 | let view = frame
294 | .texture
295 | .create_view(&wgpu::TextureViewDescriptor::default());
296 |
297 | {
298 | let mut render_pass =
299 | Scene::clear(&view, &mut encoder, program.background_color());
300 | scene.draw(&mut render_pass);
301 | }
302 |
303 | renderer.present::(
304 | engine,
305 | device,
306 | queue,
307 | &mut encoder,
308 | None,
309 | frame.texture.format(),
310 | &view,
311 | &app_data.viewport,
312 | &[],
313 | );
314 |
315 | engine.submit(queue, encoder);
316 | frame.present();
317 |
318 | window.set_cursor(iced_winit::conversion::mouse_interaction(
319 | state.mouse_interaction(),
320 | ));
321 | }
322 | Err(error) => match error {
323 | wgpu::SurfaceError::OutOfMemory => {
324 | panic!(
325 | "Swapchain error: {error}. \
326 | Rendering cannot continue."
327 | )
328 | }
329 | _ => {
330 | window.request_redraw();
331 | }
332 | },
333 | }
334 | }
335 | WindowEvent::CursorMoved { position, .. } => {
336 | self.cursor_position = Some(position);
337 | }
338 | WindowEvent::Touch(touch) => {
339 | self.cursor_position = Some(touch.location);
340 | }
341 | WindowEvent::Ime(ref ime) => {
342 | log::info!("Ime event: {:?}", ime);
343 | }
344 | WindowEvent::ModifiersChanged(modifiers) => {
345 | self.modifiers = modifiers.state();
346 | }
347 | WindowEvent::KeyboardInput {
348 | device_id: _,
349 | ref event,
350 | is_synthetic: _,
351 | } => {
352 | if let PhysicalKey::Code(code) = event.physical_key {
353 | match code {
354 | KeyCode::ShiftLeft | KeyCode::ShiftRight => match event.state {
355 | ElementState::Pressed => self.modifiers |= ModifiersState::SHIFT,
356 | ElementState::Released => self.modifiers &= !ModifiersState::SHIFT,
357 | },
358 | KeyCode::ControlLeft | KeyCode::ControlRight => match event.state {
359 | ElementState::Pressed => self.modifiers |= ModifiersState::CONTROL,
360 | ElementState::Released => self.modifiers &= !ModifiersState::CONTROL,
361 | },
362 | _ => (),
363 | }
364 | }
365 | }
366 | WindowEvent::Resized(_) => {
367 | self.resized = true;
368 | }
369 | _ => (),
370 | }
371 |
372 | if let Some(event) =
373 | iced_winit::conversion::window_event(event, window.scale_factor(), self.modifiers)
374 | {
375 | state.queue_event(event);
376 | }
377 |
378 | if !state.is_queue_empty() {
379 | let _ = state.update(
380 | app_data.viewport.logical_size(),
381 | self.cursor_position
382 | .map(|p| conversion::cursor_position(p, app_data.viewport.scale_factor()))
383 | .map(mouse::Cursor::Available)
384 | .unwrap_or(mouse::Cursor::Unavailable),
385 | renderer,
386 | &Theme::Ferra,
387 | &renderer::Style {
388 | text_color: Theme::Ferra.palette().text,
389 | },
390 | clipboard,
391 | debug,
392 | );
393 |
394 | window.request_redraw();
395 | }
396 | }
397 |
398 | fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {}
399 | }
400 |
--------------------------------------------------------------------------------
/GameActivity/src/scene.rs:
--------------------------------------------------------------------------------
1 | use iced_wgpu::wgpu;
2 | use iced_winit::core::Color;
3 |
4 | pub struct Scene {
5 | pipeline: wgpu::RenderPipeline,
6 | }
7 |
8 | impl Scene {
9 | pub fn new(device: &wgpu::Device, texture_format: wgpu::TextureFormat) -> Scene {
10 | let pipeline = build_pipeline(device, texture_format);
11 |
12 | Scene { pipeline }
13 | }
14 |
15 | pub fn clear<'a>(
16 | target: &'a wgpu::TextureView,
17 | encoder: &'a mut wgpu::CommandEncoder,
18 | background_color: Color,
19 | ) -> wgpu::RenderPass<'a> {
20 | encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
21 | label: None,
22 | color_attachments: &[Some(wgpu::RenderPassColorAttachment {
23 | view: target,
24 | resolve_target: None,
25 | ops: wgpu::Operations {
26 | load: wgpu::LoadOp::Clear({
27 | let [r, g, b, a] = background_color.into_linear();
28 |
29 | wgpu::Color {
30 | r: r as f64,
31 | g: g as f64,
32 | b: b as f64,
33 | a: a as f64,
34 | }
35 | }),
36 | store: wgpu::StoreOp::Store,
37 | },
38 | })],
39 | depth_stencil_attachment: None,
40 | timestamp_writes: None,
41 | occlusion_query_set: None,
42 | })
43 | }
44 |
45 | pub fn draw<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
46 | render_pass.set_pipeline(&self.pipeline);
47 | render_pass.draw(0..3, 0..1);
48 | }
49 | }
50 |
51 | fn build_pipeline(
52 | device: &wgpu::Device,
53 | texture_format: wgpu::TextureFormat,
54 | ) -> wgpu::RenderPipeline {
55 | let (vs_module, fs_module) = (
56 | device.create_shader_module(wgpu::include_wgsl!("vert.wgsl")),
57 | device.create_shader_module(wgpu::include_wgsl!("frag.wgsl")),
58 | );
59 |
60 | let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
61 | label: None,
62 | push_constant_ranges: &[],
63 | bind_group_layouts: &[],
64 | });
65 |
66 | device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
67 | label: None,
68 | layout: Some(&pipeline_layout),
69 | vertex: wgpu::VertexState {
70 | module: &vs_module,
71 | entry_point: "main",
72 | buffers: &[],
73 | compilation_options: wgpu::PipelineCompilationOptions::default(),
74 | },
75 | fragment: Some(wgpu::FragmentState {
76 | module: &fs_module,
77 | entry_point: "main",
78 | targets: &[Some(wgpu::ColorTargetState {
79 | format: texture_format,
80 | blend: Some(wgpu::BlendState {
81 | color: wgpu::BlendComponent::REPLACE,
82 | alpha: wgpu::BlendComponent::REPLACE,
83 | }),
84 | write_mask: wgpu::ColorWrites::ALL,
85 | })],
86 | compilation_options: wgpu::PipelineCompilationOptions::default(),
87 | }),
88 | primitive: wgpu::PrimitiveState {
89 | topology: wgpu::PrimitiveTopology::TriangleList,
90 | front_face: wgpu::FrontFace::Ccw,
91 | ..Default::default()
92 | },
93 | depth_stencil: None,
94 | multisample: wgpu::MultisampleState {
95 | count: 1,
96 | mask: !0,
97 | alpha_to_coverage_enabled: false,
98 | },
99 | multiview: None,
100 | cache: None,
101 | })
102 | }
103 |
--------------------------------------------------------------------------------
/GameActivity/src/vert.wgsl:
--------------------------------------------------------------------------------
1 | @vertex
2 | fn main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4 {
3 | let x = f32(1 - i32(in_vertex_index)) * 0.5;
4 | let y = f32(1 - i32(in_vertex_index & 1u) * 2) * 0.5;
5 | return vec4(x, y, 0.0, 1.0);
6 | }
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2024 Ilya Baryshnikov, contributors
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software is furnished to do so,
8 | subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/NativeActivity/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "android-iced-example"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [lib]
7 | name = "example"
8 | crate-type = ["cdylib"]
9 |
10 | [dependencies]
11 | log = "0.4"
12 | android_logger = "0.14.1"
13 | android-activity = { version = "0.6", features = ["native-activity"] }
14 | ndk-context = "0.1"
15 | jni = { version = "0.21", features = ["invocation"] }
16 | # ndk-sys = "0.6.0"
17 | # ndk = "0.9.0"
18 | futures = "0.3"
19 |
20 | [dependencies.iced_core]
21 | git = "https://github.com/ibaryshnikov/iced.git"
22 | rev = "009bf6c"
23 | # path = "../../iced/core"
24 |
25 | [dependencies.iced_widget]
26 | git = "https://github.com/ibaryshnikov/iced.git"
27 | rev = "009bf6c"
28 | # path = "../../iced/widget"
29 | features = ["wgpu"]
30 |
31 | [dependencies.iced_winit]
32 | git = "https://github.com/ibaryshnikov/iced.git"
33 | rev = "009bf6c"
34 | # path = "../../iced/winit"
35 |
36 | [dependencies.iced_wgpu]
37 | git = "https://github.com/ibaryshnikov/iced.git"
38 | rev = "009bf6c"
39 | # path = "../../iced/wgpu"
40 |
41 | [patch.crates-io]
42 | softbuffer = { git = "https://github.com/MarijnS95/softbuffer.git", rev = "d5cc95a" } # branch = "android"
43 |
--------------------------------------------------------------------------------
/NativeActivity/README.md:
--------------------------------------------------------------------------------
1 | # Example of building android app with iced
2 |
3 | This is a `NativeActivity` example, based on `na-mainloop` from
4 | [android-activity](https://github.com/rust-mobile/android-activity)
5 |
6 |
7 | ## Building and running
8 |
9 | Check `android-activity` crate for detailed instructions.
10 | During my tests I was running the following command and using android studio afterwards:
11 |
12 | ```bash
13 | export ANDROID_NDK_HOME="path/to/ndk"
14 | export ANDROID_HOME="path/to/sdk"
15 |
16 | rustup target add x86_64-linux-android
17 | cargo install cargo-ndk
18 |
19 | cargo ndk -t x86_64 -o app/src/main/jniLibs/ build
20 | ```
21 |
--------------------------------------------------------------------------------
/NativeActivity/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/NativeActivity/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | }
4 |
5 | android {
6 | ndkVersion "25.2.9519653"
7 | compileSdk 35
8 |
9 | defaultConfig {
10 | applicationId "co.realfit.example"
11 | minSdk 28
12 | targetSdk 35
13 | versionCode 1
14 | versionName "1.0"
15 |
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 | }
18 |
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
23 | }
24 | debug {
25 | minifyEnabled false
26 | //packagingOptions {
27 | // doNotStrip '**/*.so'
28 | //}
29 | //debuggable true
30 | }
31 | }
32 | compileOptions {
33 | sourceCompatibility JavaVersion.VERSION_1_8
34 | targetCompatibility JavaVersion.VERSION_1_8
35 | }
36 | namespace 'co.realfit.example'
37 | }
38 |
39 | dependencies {
40 | // implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22"
41 | }
42 |
--------------------------------------------------------------------------------
/NativeActivity/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
--------------------------------------------------------------------------------
/NativeActivity/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/NativeActivity/app/src/main/java/co/realfit/example/MainActivity.java:
--------------------------------------------------------------------------------
1 | package co.realfit.example;
2 |
3 | import android.app.NativeActivity;
4 | import android.content.ClipData;
5 | import android.content.ClipboardManager;
6 | import android.content.Context;
7 | import android.os.Bundle;
8 | import android.util.Log;
9 | import android.view.inputmethod.InputMethodManager;
10 |
11 | public class MainActivity extends NativeActivity {
12 |
13 | static {
14 | System.loadLibrary("example");
15 | }
16 |
17 | @Override
18 | protected void onCreate(Bundle savedInstanceState) {
19 | super.onCreate(savedInstanceState);
20 | }
21 |
22 | private void showKeyboard() {
23 | Log.d("MainActivity", "showKeyboard instance method called");
24 | InputMethodManager inputManager = getSystemService(InputMethodManager.class);
25 | inputManager.showSoftInput(getWindow().getDecorView(), InputMethodManager.SHOW_IMPLICIT);
26 | }
27 |
28 | private void hideKeyboard() {
29 | Log.d("MainActivity", "hideKeyboard instance method called");
30 | InputMethodManager inputManager = getSystemService(InputMethodManager.class);
31 | inputManager.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0);
32 | }
33 |
34 | private String readClipboard() {
35 | ClipboardManager clipboardManager = (ClipboardManager) getApplicationContext().getSystemService(Context.CLIPBOARD_SERVICE);
36 | ClipData data = clipboardManager.getPrimaryClip();
37 | if (data == null) {
38 | Log.d("MainActivity", "ClipData in readClipboard is null");
39 | return "";
40 | }
41 | ClipData.Item item = data.getItemAt(0);
42 | if (item == null) {
43 | Log.d("MainActivity", "Item in readClipboard is null");
44 | return "";
45 | }
46 | return item.coerceToText(this).toString();
47 | }
48 |
49 | private void writeClipboard(String value) {
50 | ClipboardManager clipboardManager = (ClipboardManager) getApplicationContext().getSystemService(Context.CLIPBOARD_SERVICE);
51 | ClipData data = ClipData.newPlainText("MainActivity text", value);
52 | clipboardManager.setPrimaryClip(data);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/NativeActivity/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/NativeActivity/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 |
--------------------------------------------------------------------------------
/NativeActivity/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/NativeActivity/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/NativeActivity/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/NativeActivity/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/NativeActivity/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/NativeActivity/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/NativeActivity/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/NativeActivity/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/NativeActivity/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/NativeActivity/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/NativeActivity/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/NativeActivity/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/NativeActivity/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/NativeActivity/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/NativeActivity/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/NativeActivity/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/NativeActivity/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/NativeActivity/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/NativeActivity/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/NativeActivity/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/NativeActivity/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/NativeActivity/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/NativeActivity/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application' version '8.8.0' apply false
3 | id 'com.android.library' version '8.8.0' apply false
4 | // id 'org.jetbrains.kotlin.android' version '1.8.22' apply false
5 | }
6 |
7 | tasks.register('clean', Delete) {
8 | delete rootProject.layout.buildDirectory
9 | }
10 |
--------------------------------------------------------------------------------
/NativeActivity/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | cargo ndk -t x86_64 -o app/src/main/jniLibs/ build
4 |
--------------------------------------------------------------------------------
/NativeActivity/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
--------------------------------------------------------------------------------
/NativeActivity/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/NativeActivity/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/NativeActivity/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.10.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/NativeActivity/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 |
--------------------------------------------------------------------------------
/NativeActivity/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 |
--------------------------------------------------------------------------------
/NativeActivity/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 | //rootProject.name = "Rust Template"
16 | include ':app'
17 |
--------------------------------------------------------------------------------
/NativeActivity/src/clipboard.rs:
--------------------------------------------------------------------------------
1 | use jni::objects::{JObject, JString, JValue};
2 |
3 | use crate::java::{get_env, get_vm};
4 |
5 | pub(crate) struct Clipboard {}
6 |
7 | impl iced_core::Clipboard for Clipboard {
8 | fn read(&self, _kind: iced_core::clipboard::Kind) -> Option {
9 | log::debug!("Clipboard read method called");
10 | match read_clipboard() {
11 | Ok(text) => Some(text),
12 | Err(e) => {
13 | log::error!("Error reading from clipboard: {e}");
14 | None
15 | }
16 | }
17 | }
18 | fn write(&mut self, _kind: iced_core::clipboard::Kind, contents: String) {
19 | log::debug!("Clipboard write method called");
20 | if let Err(e) = write_clipboard(contents) {
21 | log::error!("Error writing to clipboard: {e}");
22 | }
23 | }
24 | }
25 |
26 | fn read_clipboard() -> jni::errors::Result {
27 | let ctx = ndk_context::android_context();
28 | let vm = get_vm(&ctx);
29 | let mut env = get_env(&vm);
30 | let activity = unsafe { JObject::from_raw(ctx.context() as _) };
31 | let j_object = env
32 | .call_method(activity, "readClipboard", "()Ljava/lang/String;", &[])?
33 | .l()?;
34 | let j_string = JString::from(j_object);
35 | env.get_string(&j_string).map(Into::into)
36 | }
37 | fn write_clipboard(contents: String) -> jni::errors::Result<()> {
38 | let ctx = ndk_context::android_context();
39 | let vm = get_vm(&ctx);
40 | let mut env = get_env(&vm);
41 | let activity = unsafe { JObject::from_raw(ctx.context() as _) };
42 | let value = env.new_string(contents)?;
43 | let args = [JValue::Object(value.as_ref())];
44 | env.call_method(activity, "writeClipboard", "(Ljava/lang/String;)V", &args)?;
45 | Ok(())
46 | }
47 |
--------------------------------------------------------------------------------
/NativeActivity/src/controls.rs:
--------------------------------------------------------------------------------
1 | use iced_wgpu::Renderer;
2 | use iced_widget::{
3 | button, column, container, horizontal_space, pick_list, row, slider, text, text_editor,
4 | text_input, vertical_space, PickList, Slider, Space,
5 | };
6 | use iced_winit::core::{Alignment, Color, Element, Length, Theme};
7 | use iced_winit::runtime::{Program, Task};
8 | use iced_winit::winit::event_loop::EventLoopProxy;
9 |
10 | use crate::UserEvent;
11 |
12 | const EXAMPLES: [Example; 3] = [Example::Integration, Example::Counter, Example::TextEditor];
13 |
14 | #[derive(Debug, Clone, Copy, PartialEq, Eq)]
15 | pub enum Example {
16 | Integration,
17 | Counter,
18 | TextEditor,
19 | }
20 |
21 | impl std::fmt::Display for Example {
22 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 | write!(f, "{:?}", self)
24 | }
25 | }
26 |
27 | pub struct Controls {
28 | background_color: Color,
29 | input: String,
30 | value: i32,
31 | selected_example: Example,
32 | editor: text_editor::Content,
33 | proxy: EventLoopProxy,
34 | }
35 |
36 | #[derive(Debug, Clone)]
37 | pub enum Message {
38 | RedChanged(f32),
39 | GreenChanged(f32),
40 | BlueChanged(f32),
41 | InputChanged(String),
42 | EditorAction(text_editor::Action),
43 | ExampleSelected(Example),
44 | Inc,
45 | Dec,
46 | }
47 |
48 | impl Controls {
49 | pub fn new(proxy: EventLoopProxy) -> Controls {
50 | Controls {
51 | background_color: Color::BLACK,
52 | input: String::default(),
53 | value: 0,
54 | selected_example: Example::Integration,
55 | editor: text_editor::Content::new(),
56 | proxy,
57 | }
58 | }
59 |
60 | pub fn background_color(&self) -> Color {
61 | self.background_color
62 | }
63 | }
64 |
65 | impl Program for Controls {
66 | type Theme = Theme;
67 | type Message = Message;
68 | type Renderer = Renderer;
69 |
70 | fn update(&mut self, message: Message) -> Task {
71 | match message {
72 | Message::Inc => self.value += 1,
73 | Message::Dec => self.value -= 1,
74 | Message::ExampleSelected(example) => self.selected_example = example,
75 | Message::InputChanged(value) => self.input = value,
76 | Message::RedChanged(r) => self.background_color.r = r,
77 | Message::GreenChanged(g) => self.background_color.g = g,
78 | Message::BlueChanged(b) => self.background_color.b = b,
79 | Message::EditorAction(action) => match action {
80 | text_editor::Action::Focus => {
81 | log::info!("Editor focused");
82 | // it's possible to call java::call_instance_method("showKeyboard")
83 | // right here, but needed something to show the usage of user events
84 | let _ = self.proxy.send_event(UserEvent::ShowKeyboard);
85 | }
86 | text_editor::Action::Blur => {
87 | log::info!("Editor lost focus");
88 | let _ = self.proxy.send_event(UserEvent::HideKeyboard);
89 | }
90 | other => self.editor.perform(other),
91 | },
92 | }
93 |
94 | Task::none()
95 | }
96 |
97 | fn view(&self) -> Element {
98 | match self.selected_example {
99 | Example::Integration => self.integration(),
100 | Example::Counter => self.counter(),
101 | Example::TextEditor => self.text_editor(),
102 | }
103 | }
104 | }
105 |
106 | fn color_slider<'a>(value: f32, f: impl Fn(f32) -> Message + 'a) -> Slider<'a, f32, Message> {
107 | slider(0.0..=1.0, value, f).step(0.01)
108 | }
109 |
110 | impl Controls {
111 | fn examples(&self) -> PickList {
112 | pick_list(
113 | &EXAMPLES[..],
114 | Some(self.selected_example),
115 | Message::ExampleSelected,
116 | )
117 | }
118 | fn integration(&self) -> Element {
119 | let sliders = row![
120 | color_slider(self.background_color.r, Message::RedChanged),
121 | color_slider(self.background_color.g, Message::GreenChanged),
122 | color_slider(self.background_color.b, Message::BlueChanged),
123 | ]
124 | .width(Length::Fill)
125 | .spacing(20);
126 |
127 | container(
128 | column![
129 | Space::with_height(20),
130 | self.examples(),
131 | vertical_space(),
132 | row![
133 | text!("{:?}", self.background_color).size(14),
134 | horizontal_space(),
135 | ],
136 | text_input("Placeholder", &self.input).on_input(Message::InputChanged),
137 | sliders,
138 | Space::with_height(20),
139 | ]
140 | .align_x(Alignment::Center)
141 | .spacing(10),
142 | )
143 | .padding(10)
144 | .into()
145 | }
146 |
147 | fn counter(&self) -> Element {
148 | container(
149 | column![
150 | Space::with_height(30),
151 | self.examples(),
152 | vertical_space(),
153 | button("Increment").on_press(Message::Inc),
154 | text!("{}", self.value).size(50),
155 | button("Decrement").on_press(Message::Dec),
156 | vertical_space(),
157 | Space::with_height(100),
158 | ]
159 | .align_x(Alignment::Center)
160 | .spacing(10),
161 | )
162 | .center(Length::Fill)
163 | .style(add_background)
164 | .into()
165 | }
166 |
167 | fn text_editor(&self) -> Element {
168 | container(
169 | column![
170 | Space::with_height(30),
171 | self.examples(),
172 | vertical_space(),
173 | text_editor::(&self.editor)
174 | .height(400)
175 | .on_action(Message::EditorAction),
176 | vertical_space(),
177 | ]
178 | .align_x(Alignment::Center),
179 | )
180 | .padding(10)
181 | .center(Length::Fill)
182 | .style(add_background)
183 | .into()
184 | }
185 | }
186 |
187 | fn add_background(theme: &Theme) -> container::Style {
188 | theme.palette().background.into()
189 | }
190 |
--------------------------------------------------------------------------------
/NativeActivity/src/frag.wgsl:
--------------------------------------------------------------------------------
1 | @fragment
2 | fn main() -> @location(0) vec4 {
3 | return vec4(1.0, 0.0, 0.0, 1.0);
4 | }
5 |
--------------------------------------------------------------------------------
/NativeActivity/src/java.rs:
--------------------------------------------------------------------------------
1 | use jni::objects::JObject;
2 | use jni::{AttachGuard, JavaVM};
3 |
4 | //
5 | // some jni syntax hints
6 | //
7 | // L - class, for example: Ljava/lang/String;
8 | // primitives: Z - boolean, I - integer, V - void
9 | // for example, Rust signature fn get_text(flag: bool) -> String
10 | // will become "(Z)Ljava/lang/String;"
11 | //
12 | // in find_class . is replaced by $
13 | // docs: android/view/WindowManager.LayoutParams
14 | // jni: android/view/WindowManager$LayoutParams
15 | // it also has some quirks:
16 | // https://developer.android.com/training/articles/perf-jni.html#faq_FindClass
17 | //
18 |
19 | pub(crate) fn call_instance_method(name: &str) {
20 | log::debug!("Calling instance method from Rust: {}", name);
21 | let ctx = ndk_context::android_context();
22 | let vm = get_vm(&ctx);
23 | let mut env = get_env(&vm);
24 | let activity = unsafe { JObject::from_raw(ctx.context() as _) };
25 | if let Err(e) = env.call_method(activity, name, "()V", &[]) {
26 | log::error!("Error calling instance method {}: {}", name, e);
27 | }
28 | }
29 |
30 | pub(crate) fn get_vm(ctx: &ndk_context::AndroidContext) -> JavaVM {
31 | unsafe { JavaVM::from_raw(ctx.vm() as _) }.unwrap_or_else(|e| {
32 | log::error!("Error getting ctx.vm(): {:?}", e);
33 | panic!("No JavaVM found");
34 | })
35 | }
36 |
37 | pub(crate) fn get_env(vm: &JavaVM) -> AttachGuard {
38 | vm.attach_current_thread().unwrap_or_else(|e| {
39 | log::error!("Error attaching vm: {:?}", e);
40 | panic!("Failed to call attach_current_thread for JavaVM");
41 | })
42 | }
43 |
--------------------------------------------------------------------------------
/NativeActivity/src/lib.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Arc;
2 | use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
3 |
4 | use iced_wgpu::graphics::Viewport;
5 | use iced_wgpu::{wgpu, Engine, Renderer};
6 | use iced_winit::core::{mouse, renderer, Font, Pixels, Size, Theme};
7 | use iced_winit::runtime::{program, Debug};
8 | use iced_winit::{conversion, winit};
9 | use log::LevelFilter;
10 | use wgpu::{Device, Instance, Queue, TextureFormat};
11 | use winit::application::ApplicationHandler;
12 | use winit::event::{DeviceEvent, DeviceId, ElementState, StartCause, WindowEvent};
13 | use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy};
14 | use winit::keyboard::{KeyCode, ModifiersState, PhysicalKey};
15 | use winit::platform::android::activity::AndroidApp;
16 | use winit::platform::android::EventLoopBuilderExtAndroid;
17 | use winit::window::{Window, WindowId};
18 |
19 | mod clipboard;
20 | mod controls;
21 | mod java;
22 | mod scene;
23 |
24 | use clipboard::Clipboard;
25 | use controls::Controls;
26 | use scene::Scene;
27 |
28 | // winit ime support
29 | // https://github.com/rust-windowing/winit/pull/2993
30 |
31 | // issue with android-activity crate default_motion_filter function
32 | // https://github.com/rust-mobile/android-activity/issues/79
33 |
34 | #[no_mangle]
35 | fn android_main(android_app: AndroidApp) {
36 | let logger_config = android_logger::Config::default().with_max_level(LevelFilter::Info);
37 | android_logger::init_once(logger_config);
38 |
39 | log::info!("android_main started");
40 |
41 | let event_loop = EventLoop::with_user_event()
42 | .with_android_app(android_app)
43 | .build()
44 | .expect("Should build event loop");
45 |
46 | let proxy = event_loop.create_proxy();
47 |
48 | let mut app = App::new(proxy);
49 | event_loop.run_app(&mut app).expect("Should run event loop");
50 | }
51 |
52 | #[derive(Debug)]
53 | enum UserEvent {
54 | ShowKeyboard,
55 | HideKeyboard,
56 | Tick,
57 | }
58 |
59 | struct App {
60 | proxy: EventLoopProxy,
61 | app_data: Option,
62 | resized: bool,
63 | cursor_position: Option>,
64 | modifiers: ModifiersState,
65 | value: AtomicU32,
66 | running: Arc,
67 | }
68 |
69 | struct AppData {
70 | state: program::State,
71 | scene: Scene,
72 | window: Arc,
73 | device: Device,
74 | queue: Queue,
75 | surface: wgpu::Surface<'static>,
76 | format: TextureFormat,
77 | engine: Engine,
78 | renderer: Renderer,
79 | clipboard: Clipboard,
80 | viewport: Viewport,
81 | debug: Debug,
82 | }
83 |
84 | impl App {
85 | fn new(proxy: EventLoopProxy) -> Self {
86 | Self {
87 | proxy,
88 | app_data: None,
89 | resized: false,
90 | cursor_position: None,
91 | modifiers: ModifiersState::default(),
92 | value: AtomicU32::new(0),
93 | running: Arc::new(AtomicBool::new(false)),
94 | }
95 | }
96 | }
97 |
98 | impl ApplicationHandler for App {
99 | fn new_events(&mut self, _event_loop: &ActiveEventLoop, _cause: StartCause) {
100 | // log::info!("New events cause {:?}", cause);
101 | }
102 |
103 | fn resumed(&mut self, event_loop: &ActiveEventLoop) {
104 | log::info!("Resumed");
105 | // if self.app_data.is_some() {
106 | // log::info!("Already initialized, skipping");
107 | // return;
108 | // }
109 |
110 | let instance = Instance::new(wgpu::InstanceDescriptor {
111 | backends: wgpu::Backends::all(),
112 | ..Default::default()
113 | });
114 |
115 | let attrs = Window::default_attributes();
116 | let window = Arc::new(event_loop.create_window(attrs).unwrap());
117 |
118 | let physical_size = window.inner_size();
119 | let viewport = Viewport::with_physical_size(
120 | Size::new(physical_size.width, physical_size.height),
121 | window.scale_factor(),
122 | );
123 | let clipboard = Clipboard {};
124 |
125 | let surface = instance
126 | .create_surface(window.clone())
127 | .expect("Create window surface");
128 |
129 | let (format, adapter, device, queue) = futures::executor::block_on(async {
130 | let adapter =
131 | wgpu::util::initialize_adapter_from_env_or_default(&instance, Some(&surface))
132 | .await
133 | .expect("Create adapter");
134 |
135 | let adapter_features = adapter.features();
136 |
137 | let capabilities = surface.get_capabilities(&adapter);
138 |
139 | let (device, queue) = adapter
140 | .request_device(
141 | &wgpu::DeviceDescriptor {
142 | label: None,
143 | required_features: adapter_features & wgpu::Features::default(),
144 | required_limits: wgpu::Limits::default(),
145 | memory_hints: wgpu::MemoryHints::MemoryUsage,
146 | },
147 | None,
148 | )
149 | .await
150 | .expect("Request device");
151 |
152 | (
153 | capabilities
154 | .formats
155 | .iter()
156 | .copied()
157 | .find(wgpu::TextureFormat::is_srgb)
158 | .or_else(|| capabilities.formats.first().copied())
159 | .expect("Get preferred format"),
160 | adapter,
161 | device,
162 | queue,
163 | )
164 | });
165 |
166 | surface.configure(
167 | &device,
168 | &wgpu::SurfaceConfiguration {
169 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
170 | format,
171 | width: physical_size.width,
172 | height: physical_size.height,
173 | present_mode: wgpu::PresentMode::AutoVsync,
174 | alpha_mode: wgpu::CompositeAlphaMode::Auto,
175 | view_formats: vec![],
176 | desired_maximum_frame_latency: 2,
177 | },
178 | );
179 |
180 | let scene = Scene::new(&device, format);
181 | let controls = Controls::new(self.proxy.clone());
182 |
183 | let mut debug = Debug::new();
184 | let engine = Engine::new(&adapter, &device, &queue, format, None);
185 | let mut renderer = Renderer::new(&device, &engine, Font::default(), Pixels::from(16));
186 |
187 | let state =
188 | program::State::new(controls, viewport.logical_size(), &mut renderer, &mut debug);
189 |
190 | event_loop.set_control_flow(ControlFlow::Wait);
191 |
192 | self.cursor_position = None;
193 | self.modifiers = ModifiersState::default();
194 |
195 | let app_data = AppData {
196 | state,
197 | scene,
198 | window,
199 | device,
200 | queue,
201 | surface,
202 | format,
203 | engine,
204 | renderer,
205 | clipboard,
206 | viewport,
207 | debug,
208 | };
209 | self.app_data = Some(app_data);
210 |
211 | let event_loop_running = self.running.load(Ordering::SeqCst);
212 | if event_loop_running {
213 | return;
214 | }
215 | self.running.store(true, Ordering::SeqCst);
216 |
217 | let event_proxy = self.proxy.clone();
218 | let is_running = self.running.clone();
219 |
220 | std::thread::spawn(move || {
221 | loop {
222 | std::thread::sleep(std::time::Duration::from_secs(10));
223 | if let Err(_e) = event_proxy.send_event(UserEvent::Tick) {
224 | is_running.store(false, Ordering::SeqCst);
225 | break;
226 | }
227 | }
228 | });
229 | }
230 |
231 | fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) {
232 | match event {
233 | UserEvent::ShowKeyboard => {
234 | java::call_instance_method("showKeyboard");
235 | }
236 | UserEvent::HideKeyboard => {
237 | java::call_instance_method("hideKeyboard");
238 | }
239 | UserEvent::Tick => {
240 | let value = self.value.fetch_add(1, Ordering::SeqCst);
241 | log::info!("Tick event, counter value: {}", value);
242 | }
243 | }
244 | }
245 |
246 | fn device_event(
247 | &mut self,
248 | _event_loop: &ActiveEventLoop,
249 | _device_id: DeviceId,
250 | event: DeviceEvent,
251 | ) {
252 | log::info!("DeviceEvent {:?}", event);
253 | }
254 |
255 | fn window_event(
256 | &mut self,
257 | event_loop: &ActiveEventLoop,
258 | _window_id: WindowId,
259 | event: WindowEvent,
260 | ) {
261 | log::info!("Window event: {:?}", event);
262 |
263 | let Some(app_data) = self.app_data.as_mut() else {
264 | return;
265 | };
266 |
267 | let AppData {
268 | state,
269 | scene,
270 | window,
271 | device,
272 | queue,
273 | surface,
274 | format,
275 | engine,
276 | renderer,
277 | clipboard,
278 | debug,
279 | ..
280 | } = app_data;
281 |
282 | match event {
283 | WindowEvent::CloseRequested => {
284 | event_loop.exit();
285 | }
286 | WindowEvent::RedrawRequested => {
287 | if self.resized {
288 | let size = window.inner_size();
289 |
290 | app_data.viewport = Viewport::with_physical_size(
291 | Size::new(size.width, size.height),
292 | window.scale_factor(),
293 | );
294 |
295 | surface.configure(
296 | device,
297 | &wgpu::SurfaceConfiguration {
298 | format: *format,
299 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
300 | width: size.width,
301 | height: size.height,
302 | present_mode: wgpu::PresentMode::AutoVsync,
303 | alpha_mode: wgpu::CompositeAlphaMode::Auto,
304 | view_formats: vec![],
305 | desired_maximum_frame_latency: 2,
306 | },
307 | );
308 |
309 | self.resized = false;
310 | }
311 |
312 | match surface.get_current_texture() {
313 | Ok(frame) => {
314 | let mut encoder =
315 | device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
316 | label: None,
317 | });
318 |
319 | let program = state.program();
320 |
321 | let view = frame
322 | .texture
323 | .create_view(&wgpu::TextureViewDescriptor::default());
324 |
325 | {
326 | let mut render_pass =
327 | Scene::clear(&view, &mut encoder, program.background_color());
328 | scene.draw(&mut render_pass);
329 | }
330 |
331 | renderer.present::(
332 | engine,
333 | device,
334 | queue,
335 | &mut encoder,
336 | None,
337 | frame.texture.format(),
338 | &view,
339 | &app_data.viewport,
340 | &[],
341 | );
342 |
343 | engine.submit(queue, encoder);
344 | frame.present();
345 |
346 | window.set_cursor(iced_winit::conversion::mouse_interaction(
347 | state.mouse_interaction(),
348 | ));
349 | }
350 | Err(error) => match error {
351 | wgpu::SurfaceError::OutOfMemory => {
352 | panic!(
353 | "Swapchain error: {error}. \
354 | Rendering cannot continue."
355 | )
356 | }
357 | _ => {
358 | window.request_redraw();
359 | }
360 | },
361 | }
362 | }
363 | WindowEvent::CursorMoved { position, .. } => {
364 | self.cursor_position = Some(position);
365 | }
366 | WindowEvent::Touch(touch) => {
367 | self.cursor_position = Some(touch.location);
368 | }
369 | WindowEvent::ModifiersChanged(modifiers) => {
370 | self.modifiers = modifiers.state();
371 | }
372 | WindowEvent::KeyboardInput {
373 | device_id: _,
374 | ref event,
375 | is_synthetic: _,
376 | } => {
377 | if let PhysicalKey::Code(code) = event.physical_key {
378 | match code {
379 | KeyCode::ShiftLeft | KeyCode::ShiftRight => match event.state {
380 | ElementState::Pressed => self.modifiers |= ModifiersState::SHIFT,
381 | ElementState::Released => self.modifiers &= !ModifiersState::SHIFT,
382 | },
383 | KeyCode::ControlLeft | KeyCode::ControlRight => match event.state {
384 | ElementState::Pressed => self.modifiers |= ModifiersState::CONTROL,
385 | ElementState::Released => self.modifiers &= !ModifiersState::CONTROL,
386 | },
387 | _ => (),
388 | }
389 | }
390 | }
391 | WindowEvent::Resized(_) => {
392 | self.resized = true;
393 | }
394 | _ => (),
395 | }
396 |
397 | if let Some(event) =
398 | iced_winit::conversion::window_event(event, window.scale_factor(), self.modifiers)
399 | {
400 | state.queue_event(event);
401 | }
402 |
403 | if !state.is_queue_empty() {
404 | let _ = state.update(
405 | app_data.viewport.logical_size(),
406 | self.cursor_position
407 | .map(|p| conversion::cursor_position(p, app_data.viewport.scale_factor()))
408 | .map(mouse::Cursor::Available)
409 | .unwrap_or(mouse::Cursor::Unavailable),
410 | renderer,
411 | &Theme::Ferra,
412 | &renderer::Style {
413 | text_color: Theme::Ferra.palette().text,
414 | },
415 | clipboard,
416 | debug,
417 | );
418 |
419 | window.request_redraw();
420 | }
421 | }
422 |
423 | fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {}
424 | }
425 |
--------------------------------------------------------------------------------
/NativeActivity/src/scene.rs:
--------------------------------------------------------------------------------
1 | use iced_wgpu::wgpu;
2 | use iced_winit::core::Color;
3 |
4 | pub struct Scene {
5 | pipeline: wgpu::RenderPipeline,
6 | }
7 |
8 | impl Scene {
9 | pub fn new(device: &wgpu::Device, texture_format: wgpu::TextureFormat) -> Scene {
10 | let pipeline = build_pipeline(device, texture_format);
11 |
12 | Scene { pipeline }
13 | }
14 |
15 | pub fn clear<'a>(
16 | target: &'a wgpu::TextureView,
17 | encoder: &'a mut wgpu::CommandEncoder,
18 | background_color: Color,
19 | ) -> wgpu::RenderPass<'a> {
20 | encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
21 | label: None,
22 | color_attachments: &[Some(wgpu::RenderPassColorAttachment {
23 | view: target,
24 | resolve_target: None,
25 | ops: wgpu::Operations {
26 | load: wgpu::LoadOp::Clear({
27 | let [r, g, b, a] = background_color.into_linear();
28 |
29 | wgpu::Color {
30 | r: r as f64,
31 | g: g as f64,
32 | b: b as f64,
33 | a: a as f64,
34 | }
35 | }),
36 | store: wgpu::StoreOp::Store,
37 | },
38 | })],
39 | depth_stencil_attachment: None,
40 | timestamp_writes: None,
41 | occlusion_query_set: None,
42 | })
43 | }
44 |
45 | pub fn draw<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
46 | render_pass.set_pipeline(&self.pipeline);
47 | render_pass.draw(0..3, 0..1);
48 | }
49 | }
50 |
51 | fn build_pipeline(
52 | device: &wgpu::Device,
53 | texture_format: wgpu::TextureFormat,
54 | ) -> wgpu::RenderPipeline {
55 | let (vs_module, fs_module) = (
56 | device.create_shader_module(wgpu::include_wgsl!("vert.wgsl")),
57 | device.create_shader_module(wgpu::include_wgsl!("frag.wgsl")),
58 | );
59 |
60 | let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
61 | label: None,
62 | push_constant_ranges: &[],
63 | bind_group_layouts: &[],
64 | });
65 |
66 | device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
67 | label: None,
68 | layout: Some(&pipeline_layout),
69 | vertex: wgpu::VertexState {
70 | module: &vs_module,
71 | entry_point: "main",
72 | buffers: &[],
73 | compilation_options: wgpu::PipelineCompilationOptions::default(),
74 | },
75 | fragment: Some(wgpu::FragmentState {
76 | module: &fs_module,
77 | entry_point: "main",
78 | targets: &[Some(wgpu::ColorTargetState {
79 | format: texture_format,
80 | blend: Some(wgpu::BlendState {
81 | color: wgpu::BlendComponent::REPLACE,
82 | alpha: wgpu::BlendComponent::REPLACE,
83 | }),
84 | write_mask: wgpu::ColorWrites::ALL,
85 | })],
86 | compilation_options: wgpu::PipelineCompilationOptions::default(),
87 | }),
88 | primitive: wgpu::PrimitiveState {
89 | topology: wgpu::PrimitiveTopology::TriangleList,
90 | front_face: wgpu::FrontFace::Ccw,
91 | ..Default::default()
92 | },
93 | depth_stencil: None,
94 | multisample: wgpu::MultisampleState {
95 | count: 1,
96 | mask: !0,
97 | alpha_to_coverage_enabled: false,
98 | },
99 | multiview: None,
100 | cache: None,
101 | })
102 | }
103 |
--------------------------------------------------------------------------------
/NativeActivity/src/vert.wgsl:
--------------------------------------------------------------------------------
1 | @vertex
2 | fn main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4 {
3 | let x = f32(1 - i32(in_vertex_index)) * 0.5;
4 | let y = f32(1 - i32(in_vertex_index & 1u) * 2) * 0.5;
5 | return vec4(x, y, 0.0, 1.0);
6 | }
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Example of building android app with iced
2 |
3 | There are [NativeActivity](./NativeActivity) and [GameActivity](./GameActivity) examples here.
4 |
5 | Based on several other examples:
6 | - `na-mainloop` and `agdk-mainloop` from
7 | [android-activity](https://github.com/rust-mobile/android-activity/tree/v0.6.0/examples)
8 | - [na-winit-wgpu](https://github.com/rust-mobile/rust-android-examples/tree/main/na-winit-wgpu)
9 | from `rust-android-examples`
10 | - [integration](https://github.com/iced-rs/iced/tree/0.13.1/examples/integration)
11 | from `iced`
12 |
13 |
14 | ## Preview
15 |
16 | iced integration example
17 |
18 | 
19 | 
20 |
21 | You can also run most of the examples from iced.
22 | For this omit the scene rendering part and set the background of the root container.
23 |
24 |
25 | ## Watch
26 |
27 | 
28 | 
29 | 
30 |
31 |
32 | ## Text input
33 |
34 | Text input partially works, unresolved issues:
35 | - window doesn't resize on show/hide soft keyboard
36 | - how to change input language of soft keyboard
37 | - ime is not supported
38 |
39 | Copy/paste and show/hide soft keyboard is implemented by calling Java
40 |
41 | 
42 |
43 |
44 | ## Building and running
45 |
46 | Check `android-activity` crate for detailed instructions.
47 | During my tests I was running the following command and using android studio afterwards:
48 |
49 | ```bash
50 | export ANDROID_NDK_HOME="path/to/ndk"
51 | export ANDROID_HOME="path/to/sdk"
52 |
53 | rustup target add x86_64-linux-android
54 | cargo install cargo-ndk
55 |
56 | cargo ndk -t x86_64 -o app/src/main/jniLibs/ build
57 | ```
58 |
59 |
60 | My setup is the following:
61 | - archlinux 6.9.6
62 | - jdk-openjdk 22
63 | - target api 35
64 |
65 |
66 | ## How it works
67 |
68 | Thanks to `android-activity` we can already build android apps in Rust, and
69 | key crates such as `winit` and `wgpu` also support building for android.
70 | `iced` doesn't support android out of the box, but it can be integrated with
71 | existing graphics pipelines, as shown in
72 | [integration](https://github.com/iced-rs/iced/tree/0.13.1/examples/integration) example.
73 | As a result, it was possible to convert existing example running `winit` + `wgpu` to
74 | use `iced` on top.
75 |
--------------------------------------------------------------------------------
/pixel_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/pixel_1.png
--------------------------------------------------------------------------------
/pixel_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/pixel_2.png
--------------------------------------------------------------------------------
/pixel_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/pixel_3.png
--------------------------------------------------------------------------------
/watch_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/watch_1.png
--------------------------------------------------------------------------------
/watch_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/watch_2.png
--------------------------------------------------------------------------------
/watch_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibaryshnikov/android-iced-example/2fb4f2c6fec82f1bfdcf91f1e87126cbbae6b8fd/watch_3.png
--------------------------------------------------------------------------------