From 13d1de302cf95187afa5027a78d302f5788d5f29 Mon Sep 17 00:00:00 2001 From: "dima.avdeev" <99798741+dima-avdeev-jb@users.noreply.github.com> Date: Tue, 22 Nov 2022 15:52:48 +0300 Subject: [PATCH] [components/resources] add resource("img.png"). rememberImageBitmap() (#2483) --- components/.gitignore | 3 +- components/README.md | 24 ++ components/build.gradle.kts | 2 + components/gradle.properties | 16 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- components/resources/demo/.gitignore | 6 + .../demo/androidApp/build.gradle.kts | 33 ++ .../androidApp/src/main/AndroidManifest.xml | 20 + .../compose/resources/demo/MainActivity.kt | 15 + .../src/main/res/values/strings.xml | 3 + .../demo/desktopApp/build.gradle.kts | 22 + .../desktopApp/src/jvmMain/kotlin/Main.kt | 18 + .../demo/iosApp/Configuration/TeamId.xcconfig | 1 + components/resources/demo/iosApp/Podfile | 5 + .../ResourcesDemo.xcodeproj/project.pbxproj | 398 ++++++++++++++++++ .../resources/demo/iosApp/iosApp/Info.plist | 48 +++ .../resources/demo/iosApp/iosApp/iosApp.swift | 15 + .../resources/demo/shared/build.gradle.kts | 101 +++++ .../src/androidMain/AndroidManifest.xml | 2 + .../resources/demo/shared/main.android.kt | 13 + .../resources/demo/shared/UseResources.kt | 23 + .../src/commonMain/resources/dir/img.png | Bin 0 -> 31449 bytes .../shared/src/commonMain/resources/img.webp | Bin 0 -> 10474 bytes .../resources/demo/shared/main.desktop.kt | 20 + .../shared/src/iosMain/kotlin/main.ios.kt | 24 ++ .../demo/shared/src/jsMain/kotlin/main.js.kt | 31 ++ .../shared/src/jsMain/resources/index.html | 16 + .../shared/src/jsMain/resources/styles.css | 8 + .../shared/src/macosMain/kotlin/main.macos.kt | 17 + components/resources/library/build.gradle.kts | 102 ++++- .../compose/resources/ComposeResourceTest.kt | 61 +++ .../src/androidAndroidTest/resources/1.png | Bin 0 -> 946 bytes .../src/androidAndroidTest/resources/2.png | Bin 0 -> 785 bytes .../src/androidMain/AndroidManifest.xml | 2 + .../resources/ComposableResource.android.kt | 18 + .../compose/resources/Resource.android.kt | 26 ++ .../resources/ComposeResource.common.kt | 40 ++ .../resources/ExperimentalResourceApi.kt | 9 + .../compose/resources/Resource.common.kt | 38 ++ .../compose/resources/ResourceTest.kt | 23 + .../compose/resources/Resource.desktop.kt | 26 ++ .../compose/resources/ComposeResourceTest.kt | 61 +++ .../library/src/desktopTest/resources/1.png | Bin 0 -> 946 bytes .../library/src/desktopTest/resources/2.png | Bin 0 -> 785 bytes .../compose/resources/Resource.ios.kt | 37 ++ .../compose/resources/Resource.js.kt | 44 ++ .../compose/resources/Resource.macos.kt | 39 ++ .../resources/ComposeResource.skiko.kt | 13 + components/settings.gradle.kts | 7 +- components/test.sh | 11 + compose/README.md | 1 + compose/scripts/publishComponentsToMavenLocal | 13 + .../configureTaskToGenerateXcodeProject.kt | 13 + 53 files changed, 1460 insertions(+), 10 deletions(-) create mode 100644 components/README.md create mode 100644 components/resources/demo/.gitignore create mode 100644 components/resources/demo/androidApp/build.gradle.kts create mode 100644 components/resources/demo/androidApp/src/main/AndroidManifest.xml create mode 100644 components/resources/demo/androidApp/src/main/kotlin/org/jetbrains/compose/resources/demo/MainActivity.kt create mode 100644 components/resources/demo/androidApp/src/main/res/values/strings.xml create mode 100644 components/resources/demo/desktopApp/build.gradle.kts create mode 100644 components/resources/demo/desktopApp/src/jvmMain/kotlin/Main.kt create mode 100644 components/resources/demo/iosApp/Configuration/TeamId.xcconfig create mode 100644 components/resources/demo/iosApp/Podfile create mode 100644 components/resources/demo/iosApp/ResourcesDemo.xcodeproj/project.pbxproj create mode 100644 components/resources/demo/iosApp/iosApp/Info.plist create mode 100644 components/resources/demo/iosApp/iosApp/iosApp.swift create mode 100644 components/resources/demo/shared/build.gradle.kts create mode 100644 components/resources/demo/shared/src/androidMain/AndroidManifest.xml create mode 100644 components/resources/demo/shared/src/androidMain/kotlin/org/jetbrains/compose/resources/demo/shared/main.android.kt create mode 100644 components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/UseResources.kt create mode 100644 components/resources/demo/shared/src/commonMain/resources/dir/img.png create mode 100644 components/resources/demo/shared/src/commonMain/resources/img.webp create mode 100644 components/resources/demo/shared/src/desktopMain/kotlin/org/jetbrains/compose/resources/demo/shared/main.desktop.kt create mode 100644 components/resources/demo/shared/src/iosMain/kotlin/main.ios.kt create mode 100644 components/resources/demo/shared/src/jsMain/kotlin/main.js.kt create mode 100644 components/resources/demo/shared/src/jsMain/resources/index.html create mode 100644 components/resources/demo/shared/src/jsMain/resources/styles.css create mode 100644 components/resources/demo/shared/src/macosMain/kotlin/main.macos.kt create mode 100644 components/resources/library/src/androidAndroidTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt create mode 100644 components/resources/library/src/androidAndroidTest/resources/1.png create mode 100644 components/resources/library/src/androidAndroidTest/resources/2.png create mode 100644 components/resources/library/src/androidMain/AndroidManifest.xml create mode 100644 components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ComposableResource.android.kt create mode 100644 components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/Resource.android.kt create mode 100644 components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ComposeResource.common.kt create mode 100644 components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ExperimentalResourceApi.kt create mode 100644 components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.common.kt create mode 100644 components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ResourceTest.kt create mode 100644 components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/Resource.desktop.kt create mode 100644 components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt create mode 100644 components/resources/library/src/desktopTest/resources/1.png create mode 100644 components/resources/library/src/desktopTest/resources/2.png create mode 100644 components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/Resource.ios.kt create mode 100644 components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt create mode 100644 components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/Resource.macos.kt create mode 100644 components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/ComposeResource.skiko.kt create mode 100755 components/test.sh create mode 100755 compose/scripts/publishComponentsToMavenLocal diff --git a/components/.gitignore b/components/.gitignore index ba8435b9c5..92750f9c5f 100644 --- a/components/.gitignore +++ b/components/.gitignore @@ -12,4 +12,5 @@ build/ /captures .externalNativeBuild -.cxx \ No newline at end of file +.cxx +kotlin-js-store diff --git a/components/README.md b/components/README.md new file mode 100644 index 0000000000..7c58b9785b --- /dev/null +++ b/components/README.md @@ -0,0 +1,24 @@ +# Libraries for Compose Multiplatform + +## Resources +Library to load resources, like images. + +### How to run demo project: +*Prerequisites*: to run on iOS and Android, you should have "Kotlin Multiplatform Mobile" plugin installed either +in Android Studio or in AppCode with [installed CocoaPods](https://kotlinlang.org/docs/native-cocoapods.html). + +### Run on desktop via Gradle: +`./gradlew :resources:demo:desktopApp:run` + +### Run JS in browser with WebAssembly Skia via Gradle: +`./gradlew :resources:demo:shared:jsBrowserDevelopmentRun` + +### Run MacOS via Gradle: + - on Intel CPU: `./gradlew :resources:demo:shared:runDebugExecutableMacosX64` + - on Apple Silicon: `./gradlew :resources:demo:shared:runDebugExecutableMacosArm64` + +# Tests +Run script: +```bash +./test.sh +``` diff --git a/components/build.gradle.kts b/components/build.gradle.kts index 7095180735..6e2452bd06 100644 --- a/components/build.gradle.kts +++ b/components/build.gradle.kts @@ -1,5 +1,6 @@ plugins { kotlin("multiplatform") apply false + id("com.android.library") apply false } subprojects { @@ -9,6 +10,7 @@ subprojects { google() mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + mavenLocal() } plugins.withId("java") { diff --git a/components/gradle.properties b/components/gradle.properties index bc6a73e091..23817d322e 100644 --- a/components/gradle.properties +++ b/components/gradle.properties @@ -5,4 +5,18 @@ kotlin.code.style=official # __KOTLIN_COMPOSE_VERSION__ kotlin.version=1.7.20 # __LATEST_COMPOSE_RELEASE_VERSION__ -compose.version=1.2.1 +compose.version=1.3.0-beta02 +agp.version=7.3.1 +org.jetbrains.compose.experimental.jscanvas.enabled=true +org.jetbrains.compose.experimental.macos.enabled=true +org.jetbrains.compose.experimental.uikit.enabled=true + +kotlin.native.cacheKind=none +kotlin.native.useEmbeddableCompilerJar=true +kotlin.native.enableDependencyPropagation=false +kotlin.mpp.enableGranularSourceSetsMetadata=true +# Enable kotlin/native experimental memory model +kotlin.native.binary.memoryModel=experimental +compose.desktop.verbose=true +kotlin.js.webpack.major.version=4 +xcodeproj=./resources/demo/iosApp diff --git a/components/gradle/wrapper/gradle-wrapper.properties b/components/gradle/wrapper/gradle-wrapper.properties index 2e6e5897b5..ae04661ee7 100644 --- a/components/gradle/wrapper/gradle-wrapper.properties +++ b/components/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/components/resources/demo/.gitignore b/components/resources/demo/.gitignore new file mode 100644 index 0000000000..3c2c4931fd --- /dev/null +++ b/components/resources/demo/.gitignore @@ -0,0 +1,6 @@ +iosApp/Podfile.lock +iosApp/Pods/* +iosApp/ResourcesDemo.xcworkspace/* +iosApp/ResourcesDemo.xcodeproj/* +!iosApp/ResourcesDemo.xcodeproj/project.pbxproj +shared/shared.podspec diff --git a/components/resources/demo/androidApp/build.gradle.kts b/components/resources/demo/androidApp/build.gradle.kts new file mode 100644 index 0000000000..4aa54613a1 --- /dev/null +++ b/components/resources/demo/androidApp/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { + id("com.android.application") + kotlin("android") +} + +dependencies { + implementation(project(":resources:demo:shared")) + implementation("androidx.appcompat:appcompat:1.5.1") + implementation("androidx.activity:activity-compose:1.6.1") + implementation("androidx.compose.foundation:foundation:1.3.1") + implementation("androidx.compose.ui:ui:1.3.1") +} + +android { + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.3.2" + } + compileSdk = 33 + defaultConfig { + applicationId = "me.user.androidApp" + minSdk = 24 + targetSdk = 33 + versionCode = 1 + versionName = "1.0" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } +} diff --git a/components/resources/demo/androidApp/src/main/AndroidManifest.xml b/components/resources/demo/androidApp/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..c5060697da --- /dev/null +++ b/components/resources/demo/androidApp/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/components/resources/demo/androidApp/src/main/kotlin/org/jetbrains/compose/resources/demo/MainActivity.kt b/components/resources/demo/androidApp/src/main/kotlin/org/jetbrains/compose/resources/demo/MainActivity.kt new file mode 100644 index 0000000000..18f3f41531 --- /dev/null +++ b/components/resources/demo/androidApp/src/main/kotlin/org/jetbrains/compose/resources/demo/MainActivity.kt @@ -0,0 +1,15 @@ +package org.jetbrains.compose.resources.demo + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import org.jetbrains.compose.resources.demo.shared.MainView + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + MainView() + } + } +} diff --git a/components/resources/demo/androidApp/src/main/res/values/strings.xml b/components/resources/demo/androidApp/src/main/res/values/strings.xml new file mode 100644 index 0000000000..bf85b09e49 --- /dev/null +++ b/components/resources/demo/androidApp/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Resources demo + \ No newline at end of file diff --git a/components/resources/demo/desktopApp/build.gradle.kts b/components/resources/demo/desktopApp/build.gradle.kts new file mode 100644 index 0000000000..3562f1fad2 --- /dev/null +++ b/components/resources/demo/desktopApp/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + kotlin("multiplatform") + id("org.jetbrains.compose") +} + +kotlin { + jvm {} + sourceSets { + val jvmMain by getting { + dependencies { + implementation(compose.desktop.currentOs) + implementation(project(":resources:demo:shared")) + } + } + } +} + +compose.desktop { + application { + mainClass = "MainKt" + } +} diff --git a/components/resources/demo/desktopApp/src/jvmMain/kotlin/Main.kt b/components/resources/demo/desktopApp/src/jvmMain/kotlin/Main.kt new file mode 100644 index 0000000000..2b4a3afd90 --- /dev/null +++ b/components/resources/demo/desktopApp/src/jvmMain/kotlin/Main.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.singleWindowApplication +import org.jetbrains.compose.resources.demo.shared.MainView + +fun main() = + singleWindowApplication( + title = "Resources demo", + state = WindowState(size = DpSize(800.dp, 800.dp)) + ) { + MainView() + } diff --git a/components/resources/demo/iosApp/Configuration/TeamId.xcconfig b/components/resources/demo/iosApp/Configuration/TeamId.xcconfig new file mode 100644 index 0000000000..bf06eb27e9 --- /dev/null +++ b/components/resources/demo/iosApp/Configuration/TeamId.xcconfig @@ -0,0 +1 @@ +TEAM_ID= diff --git a/components/resources/demo/iosApp/Podfile b/components/resources/demo/iosApp/Podfile new file mode 100644 index 0000000000..f66ae95df2 --- /dev/null +++ b/components/resources/demo/iosApp/Podfile @@ -0,0 +1,5 @@ +target 'ResourcesDemo' do + use_frameworks! + platform :ios, '14.1' + pod 'shared', :path => '../shared' +end \ No newline at end of file diff --git a/components/resources/demo/iosApp/ResourcesDemo.xcodeproj/project.pbxproj b/components/resources/demo/iosApp/ResourcesDemo.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..3f014cf4ad --- /dev/null +++ b/components/resources/demo/iosApp/ResourcesDemo.xcodeproj/project.pbxproj @@ -0,0 +1,398 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 2152FB042600AC8F00CF470E /* iosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iosApp.swift */; }; + C1FC908188C4E8695729CB06 /* Pods_ResourcesDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DE96E47030356CE6AD9794A /* Pods_ResourcesDemo.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 1EB65E27D2C0F884D0A1A133 /* Pods-ResourcesDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ResourcesDemo.debug.xcconfig"; path = "Target Support Files/Pods-ResourcesDemo/Pods-ResourcesDemo.debug.xcconfig"; sourceTree = ""; }; + 2152FB032600AC8F00CF470E /* iosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iosApp.swift; sourceTree = ""; }; + 3D7A606AB0AD7636269BD9D0 /* Pods-ResourcesDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ResourcesDemo.release.xcconfig"; path = "Target Support Files/Pods-ResourcesDemo/Pods-ResourcesDemo.release.xcconfig"; sourceTree = ""; }; + 7555FF7B242A565900829871 /* ResourcesDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ResourcesDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 8DE96E47030356CE6AD9794A /* Pods_ResourcesDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ResourcesDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AB3632DC29227652001CCB65 /* TeamId.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = TeamId.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 9964867F0862B4D9FB6ABFC7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C1FC908188C4E8695729CB06 /* Pods_ResourcesDemo.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7555FF72242A565900829871 = { + isa = PBXGroup; + children = ( + AB1DB47929225F7C00F7AF9C /* Configuration */, + 7555FF7D242A565900829871 /* iosApp */, + 7555FF7C242A565900829871 /* Products */, + E1DAFBE8E1CFC0878361EF0E /* Pods */, + B62309C7396AD7BF607A63B2 /* Frameworks */, + ); + sourceTree = ""; + }; + 7555FF7C242A565900829871 /* Products */ = { + isa = PBXGroup; + children = ( + 7555FF7B242A565900829871 /* ResourcesDemo.app */, + ); + name = Products; + sourceTree = ""; + }; + 7555FF7D242A565900829871 /* iosApp */ = { + isa = PBXGroup; + children = ( + 7555FF8C242A565B00829871 /* Info.plist */, + 2152FB032600AC8F00CF470E /* iosApp.swift */, + ); + path = iosApp; + sourceTree = ""; + }; + AB1DB47929225F7C00F7AF9C /* Configuration */ = { + isa = PBXGroup; + children = ( + AB3632DC29227652001CCB65 /* TeamId.xcconfig */, + ); + path = Configuration; + sourceTree = ""; + }; + B62309C7396AD7BF607A63B2 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 8DE96E47030356CE6AD9794A /* Pods_ResourcesDemo.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + E1DAFBE8E1CFC0878361EF0E /* Pods */ = { + isa = PBXGroup; + children = ( + 1EB65E27D2C0F884D0A1A133 /* Pods-ResourcesDemo.debug.xcconfig */, + 3D7A606AB0AD7636269BD9D0 /* Pods-ResourcesDemo.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7555FF7A242A565900829871 /* ResourcesDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "ResourcesDemo" */; + buildPhases = ( + E8D673591E7196AEA2EA10E2 /* [CP] Check Pods Manifest.lock */, + 7555FF77242A565900829871 /* Sources */, + 7555FF79242A565900829871 /* Resources */, + 9964867F0862B4D9FB6ABFC7 /* Frameworks */, + A51DDDB74597C98E89765935 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ResourcesDemo; + productName = iosApp; + productReference = 7555FF7B242A565900829871 /* ResourcesDemo.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7555FF73242A565900829871 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1130; + LastUpgradeCheck = 1130; + ORGANIZATIONNAME = org.jetbrains; + TargetAttributes = { + 7555FF7A242A565900829871 = { + CreatedOnToolsVersion = 11.3.1; + }; + }; + }; + buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "ResourcesDemo" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7555FF72242A565900829871; + productRefGroup = 7555FF7C242A565900829871 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7555FF7A242A565900829871 /* ResourcesDemo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7555FF79242A565900829871 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + A51DDDB74597C98E89765935 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ResourcesDemo/Pods-ResourcesDemo-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ResourcesDemo/Pods-ResourcesDemo-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ResourcesDemo/Pods-ResourcesDemo-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + E8D673591E7196AEA2EA10E2 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ResourcesDemo-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7555FF77242A565900829871 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2152FB042600AC8F00CF470E /* iosApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 7555FFA3242A565B00829871 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AB3632DC29227652001CCB65 /* TeamId.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 7555FFA4242A565B00829871 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AB3632DC29227652001CCB65 /* TeamId.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7555FFA6242A565B00829871 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1EB65E27D2C0F884D0A1A133 /* Pods-ResourcesDemo.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = iosApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.jetbrains.ResourcesDemo${TEAM_ID}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7555FFA7242A565B00829871 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3D7A606AB0AD7636269BD9D0 /* Pods-ResourcesDemo.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = iosApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.jetbrains.ResourcesDemo${TEAM_ID}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7555FF76242A565900829871 /* Build configuration list for PBXProject "ResourcesDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7555FFA3242A565B00829871 /* Debug */, + 7555FFA4242A565B00829871 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "ResourcesDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7555FFA6242A565B00829871 /* Debug */, + 7555FFA7242A565B00829871 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 7555FF73242A565900829871 /* Project object */; +} diff --git a/components/resources/demo/iosApp/iosApp/Info.plist b/components/resources/demo/iosApp/iosApp/Info.plist new file mode 100644 index 0000000000..9a269f5eaa --- /dev/null +++ b/components/resources/demo/iosApp/iosApp/Info.plist @@ -0,0 +1,48 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/components/resources/demo/iosApp/iosApp/iosApp.swift b/components/resources/demo/iosApp/iosApp/iosApp.swift new file mode 100644 index 0000000000..b42016a6fc --- /dev/null +++ b/components/resources/demo/iosApp/iosApp/iosApp.swift @@ -0,0 +1,15 @@ +import UIKit +import shared + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + window = UIWindow(frame: UIScreen.main.bounds) + let mainViewController = Main_iosKt.MainViewController() + window?.rootViewController = mainViewController + window?.makeKeyAndVisible() + return true + } +} diff --git a/components/resources/demo/shared/build.gradle.kts b/components/resources/demo/shared/build.gradle.kts new file mode 100644 index 0000000000..297f9775ea --- /dev/null +++ b/components/resources/demo/shared/build.gradle.kts @@ -0,0 +1,101 @@ +plugins { + kotlin("multiplatform") + kotlin("native.cocoapods") + id("com.android.library") + id("org.jetbrains.compose") +} + +version = "1.0-SNAPSHOT" + +kotlin { + android() + jvm("desktop") + ios() + iosSimulatorArm64() + js(IR) { + browser() + binaries.executable() + } + macosX64 { + binaries { + executable { + entryPoint = "main" + } + } + } + macosArm64 { + binaries { + executable { + entryPoint = "main" + } + } + } + + cocoapods { + summary = "Shared code for the sample" + homepage = "https://github.com/JetBrains/compose-jb" + ios.deploymentTarget = "14.1" + podfile = project.file("../iosApp/Podfile") + framework { + baseName = "shared" + isStatic = true + } + extraSpecAttributes["resources"] = "['src/commonMain/resources/**', 'src/iosMain/resources/**']" + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(compose.ui) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.runtime) + implementation(project(":resources:library")) + } + } + val iosMain by getting + val iosTest by getting + val iosSimulatorArm64Main by getting { + dependsOn(iosMain) + } + val iosSimulatorArm64Test by getting { + dependsOn(iosTest) + } + val desktopMain by getting { + dependencies { + implementation(compose.desktop.common) + } + } + val macosMain by creating { + dependsOn(commonMain) + } + val macosX64Main by getting { + dependsOn(macosMain) + } + val macosArm64Main by getting { + dependsOn(macosMain) + } + } +} + +android { + compileSdk = 33 + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + defaultConfig { + minSdk = 24 + targetSdk = 33 + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + sourceSets { + named("main") { + resources.srcDir("src/commonMain/resources") + } + } +} + +compose.experimental { + web.application {} +} diff --git a/components/resources/demo/shared/src/androidMain/AndroidManifest.xml b/components/resources/demo/shared/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000000..c01a0139ab --- /dev/null +++ b/components/resources/demo/shared/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/components/resources/demo/shared/src/androidMain/kotlin/org/jetbrains/compose/resources/demo/shared/main.android.kt b/components/resources/demo/shared/src/androidMain/kotlin/org/jetbrains/compose/resources/demo/shared/main.android.kt new file mode 100644 index 0000000000..37f0bf45d7 --- /dev/null +++ b/components/resources/demo/shared/src/androidMain/kotlin/org/jetbrains/compose/resources/demo/shared/main.android.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.resources.demo.shared + +import androidx.compose.runtime.Composable + +@Composable +fun MainView() { + UseResources() +} diff --git a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/UseResources.kt b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/UseResources.kt new file mode 100644 index 0000000000..9b89bae350 --- /dev/null +++ b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/UseResources.kt @@ -0,0 +1,23 @@ +package org.jetbrains.compose.resources.demo.shared + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import org.jetbrains.compose.resources.* + +@OptIn(ExperimentalResourceApi::class) +@Composable +internal fun UseResources() { + Column { + Text("Hello, resources") + Image( + bitmap = resource("dir/img.png").rememberImageBitmap().orEmpty(), + contentDescription = null, + ) + Image( + bitmap = resource("img.webp").rememberImageBitmap().orEmpty(), + contentDescription = null, + ) + } +} diff --git a/components/resources/demo/shared/src/commonMain/resources/dir/img.png b/components/resources/demo/shared/src/commonMain/resources/dir/img.png new file mode 100644 index 0000000000000000000000000000000000000000..f33b6c2622af12b1fe63d5a3f79725058e760381 GIT binary patch literal 31449 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 Km#vmI3;jP`-AaT2 literal 0 HcmV?d00001 diff --git a/components/resources/demo/shared/src/commonMain/resources/img.webp b/components/resources/demo/shared/src/commonMain/resources/img.webp new file mode 100644 index 0000000000000000000000000000000000000000..0da983e2ce5335eb13a7b793a1403588617dde3c GIT binary patch literal 10474 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 literal 0 HcmV?d00001 diff --git a/components/resources/demo/shared/src/desktopMain/kotlin/org/jetbrains/compose/resources/demo/shared/main.desktop.kt b/components/resources/demo/shared/src/desktopMain/kotlin/org/jetbrains/compose/resources/demo/shared/main.desktop.kt new file mode 100644 index 0000000000..d1997f205b --- /dev/null +++ b/components/resources/demo/shared/src/desktopMain/kotlin/org/jetbrains/compose/resources/demo/shared/main.desktop.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.resources.demo.shared + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.runtime.Composable + +@Composable +fun MainView() { + UseResources() +} + +@Preview +@Composable +fun Preview() { + MainView() +} diff --git a/components/resources/demo/shared/src/iosMain/kotlin/main.ios.kt b/components/resources/demo/shared/src/iosMain/kotlin/main.ios.kt new file mode 100644 index 0000000000..8614d4176a --- /dev/null +++ b/components/resources/demo/shared/src/iosMain/kotlin/main.ios.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.height +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Application +import org.jetbrains.compose.resources.demo.shared.UseResources +import platform.UIKit.UIViewController + +fun MainViewController(): UIViewController = + Application("Resources demo") { + Column { + Box( + modifier = Modifier + .height(100.dp) + ) + UseResources() + } + } diff --git a/components/resources/demo/shared/src/jsMain/kotlin/main.js.kt b/components/resources/demo/shared/src/jsMain/kotlin/main.js.kt new file mode 100644 index 0000000000..2eb351f69a --- /dev/null +++ b/components/resources/demo/shared/src/jsMain/kotlin/main.js.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import org.jetbrains.compose.resources.demo.shared.UseResources +import org.jetbrains.skiko.wasm.onWasmReady + +fun main() { + onWasmReady { + Window("Resources demo") { + MainView() + } + } +} + +@Composable +fun MainView() { + Column(modifier = Modifier.fillMaxSize()) { + Spacer(modifier = Modifier.height(24.dp)) + UseResources() + } +} diff --git a/components/resources/demo/shared/src/jsMain/resources/index.html b/components/resources/demo/shared/src/jsMain/resources/index.html new file mode 100644 index 0000000000..971817e6a8 --- /dev/null +++ b/components/resources/demo/shared/src/jsMain/resources/index.html @@ -0,0 +1,16 @@ + + + + + compose multiplatform web demo + + + + +

compose multiplatform web demo

+
+ +
+ + + diff --git a/components/resources/demo/shared/src/jsMain/resources/styles.css b/components/resources/demo/shared/src/jsMain/resources/styles.css new file mode 100644 index 0000000000..e5b3293a7a --- /dev/null +++ b/components/resources/demo/shared/src/jsMain/resources/styles.css @@ -0,0 +1,8 @@ +#root { + width: 100%; + height: 100vh; +} + +#root > .compose-web-column > div { + position: relative; +} \ No newline at end of file diff --git a/components/resources/demo/shared/src/macosMain/kotlin/main.macos.kt b/components/resources/demo/shared/src/macosMain/kotlin/main.macos.kt new file mode 100644 index 0000000000..963d3badf7 --- /dev/null +++ b/components/resources/demo/shared/src/macosMain/kotlin/main.macos.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +import androidx.compose.ui.window.Window +import org.jetbrains.compose.resources.demo.shared.UseResources +import platform.AppKit.NSApp +import platform.AppKit.NSApplication + +fun main() { + NSApplication.sharedApplication() + Window("Resources demo") { + UseResources() + } + NSApp?.run() +} diff --git a/components/resources/library/build.gradle.kts b/components/resources/library/build.gradle.kts index 0f4368531b..796ef6019e 100644 --- a/components/resources/library/build.gradle.kts +++ b/components/resources/library/build.gradle.kts @@ -1,24 +1,114 @@ -import org.jetbrains.compose.compose import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("multiplatform") id("org.jetbrains.compose") id("maven-publish") + id("com.android.library") } +val composeVersion = extra["compose.version"] as String + kotlin { jvm("desktop") + android { + publishLibraryVariants("release") + } + ios() + iosSimulatorArm64() + js(IR) { + browser() + } + macosX64() + macosArm64() sourceSets { - named("commonMain") { + val commonMain by getting { + dependencies { + implementation("org.jetbrains.compose.runtime:runtime:$composeVersion") + implementation("org.jetbrains.compose.foundation:foundation:$composeVersion") + } + } + val commonTest by getting { dependencies { - api(compose.runtime) - api(compose.foundation) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4") + implementation(kotlin("test")) + } + } + val skikoMain by creating { + dependsOn(commonMain) + } + val desktopMain by getting { + dependsOn(skikoMain) + } + val desktopTest by getting { + dependencies { + implementation(compose.desktop.currentOs) + implementation("org.jetbrains.compose.ui:ui-test-junit4:$composeVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.6.4") + } + } + val androidMain by getting {} + val androidTest by getting { + dependencies { + } } - named("desktopMain") {} + val iosMain by getting { + dependsOn(skikoMain) + } + val iosTest by getting + val iosSimulatorArm64Main by getting + iosSimulatorArm64Main.dependsOn(iosMain) + val iosSimulatorArm64Test by getting + iosSimulatorArm64Test.dependsOn(iosTest) + val jsMain by getting { + dependsOn(skikoMain) + } + val macosMain by creating { + dependsOn(skikoMain) + } + val macosX64Main by getting { + dependsOn(macosMain) + } + val macosArm64Main by getting { + dependsOn(macosMain) + } + } +} + +android { + compileSdk = 33 + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + defaultConfig { + minSdk = 24 + targetSdk = 33 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } + testOptions { + managedDevices { + devices { + maybeCreate("pixel5").apply { + device = "Pixel 5" + apiLevel = 31 + systemImageSource = "aosp" + } + } + } + } +} + +dependencies { + //Android integration tests + testImplementation("androidx.test:core:1.5.0") + androidTestImplementation("androidx.compose.ui:ui-test-manifest:1.3.1") + androidTestImplementation("androidx.compose.ui:ui-test:1.3.1") + androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.3.1") + androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4") } // TODO it seems that argument isn't applied to the common sourceSet. Figure out why @@ -30,4 +120,4 @@ configureMavenPublication( groupId = "org.jetbrains.compose.components", artifactId = "components-resources", name = "Resources for Compose JB" -) \ No newline at end of file +) diff --git a/components/resources/library/src/androidAndroidTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt b/components/resources/library/src/androidAndroidTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt new file mode 100644 index 0000000000..2803875019 --- /dev/null +++ b/components/resources/library/src/androidAndroidTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt @@ -0,0 +1,61 @@ +package org.jetbrains.compose.resources + +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.test.junit4.createComposeRule +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test + +@OptIn(ExperimentalResourceApi::class, ExperimentalCoroutinesApi::class) +class ComposeResourceTest { + + @get:Rule + val rule = createComposeRule() + + @Test + fun testMissingResource() = runTest (UnconfinedTestDispatcher()) { + var recompositionCount = 0 + rule.setContent { + CountRecompositions(resource("missing.png").rememberImageBitmap().orEmpty()) { + recompositionCount++ + } + } + rule.awaitIdle() + assertEquals(2, recompositionCount) + } + + @Test + fun testCountRecompositions() = runTest (UnconfinedTestDispatcher()) { + val mutableStateFlow = MutableStateFlow(true) + var recompositionCount = 0 + rule.setContent { + val state: Boolean by mutableStateFlow.collectAsState(true) + val resource = resource(if (state) "1.png" else "2.png") + CountRecompositions(resource.rememberImageBitmap().orEmpty()) { + recompositionCount++ + } + } + rule.awaitIdle() + mutableStateFlow.value = false + rule.awaitIdle() + assertEquals(4, recompositionCount) + } + +} + +@Composable +private fun CountRecompositions(imageBitmap: ImageBitmap?, onRecomposition: () -> Unit) { + onRecomposition() + if (imageBitmap != null) { + Image(bitmap = imageBitmap, contentDescription = null) + } +} diff --git a/components/resources/library/src/androidAndroidTest/resources/1.png b/components/resources/library/src/androidAndroidTest/resources/1.png new file mode 100644 index 0000000000000000000000000000000000000000..c11cb2ec658905996d5a1ecb86e8d4c6caf106b0 GIT binary patch literal 946 zcmV;j15NyiP)^G+oiJ=!Nz8&w9k}QkJa>-xI6jWIedCSZHLW4Loc(E*m zJS!k$+4{cW5f_hdmhCXZ(zT4Q{xNYoHZFx0OLl~4{UIO;0e=v!BbKPl$nf*Ol9aE8 zDHu^^Nw&B*y!-)43JeS3o-LN*0MR-oX~&4x0ZZW0G3@;FaAT9;n5D!8UBptC3rfu5@q#Etq|B$Ut^Nkn<+$PLOVF$acHl1%cq-GaU3bAv!Lq5?R zE_NZJv`cE*+W+6jW`{fElijg{jjz1}ZBpWCpE=I4yFnJ{2D@|ZQ;J6__O;8d3O0wx zf*oR0(Js#RNyW7?>?1Zg2FQX9$YJxSJbO(Vv`?HT*jynobcM}{_E|Lr1w^TaS&4Tf zhTbV*Rzs8uByLZ;=eM~)V(J15?K{FAi3{gEd>pz+Om%VQJdnfmufQVAnn;W_F^kf_ zB70DV3@|%GV(bjFKzZ(g#62rR)-k(9V(b>Py7JsJiR&pt7BPE5V(baCqVimi#Eq39 zUCf3^j14h!mFLDJPL#GWn;2(MzZg-B z@J@~gc2oEP9E2Q&U4~qTJ&1S`^C;?B+{cKoF`uJ7-{VmjP*PM@SXx|O zVM1j_X-aKQaZ+_wd0KtmfPx7PBPwQe{tqdoXGqDEmNB*2ndbCf9dZBw00000E@A-m U1WWIxpa1{>07*qoM6N<$f)}-`i~s-t literal 0 HcmV?d00001 diff --git a/components/resources/library/src/androidAndroidTest/resources/2.png b/components/resources/library/src/androidAndroidTest/resources/2.png new file mode 100644 index 0000000000000000000000000000000000000000..5fa326654ef90ee5b1db23fdf4b1f21dcd6fc9f5 GIT binary patch literal 785 zcmV+s1Md8ZP)zs)waNAO|nnWVjJc)+h^E)8qz>IiW1&#F$jelb|l-ofR=5J=#>sk)ukR9w9L+ z-cc8lkSC?;JfY0tnTRfzbcuLInZpxxQmDnNdFV2y!%uXWo2L@5v`Co;-i#j~Q{jJ9 zh>e}_#sewRGJbwSjo+w|7`vcFO4JR5e@CBd=(96SggX9_H!OXRl)08NkA~$ik9P@5 zz&9irpW`H(HY+K80E}8?4)EP4@7qr&Y7Xx}(-)8=u-*HAk#FYfV=1sj5|T#yX`X zhfg_C&%FcgQ{cV`-nYSj?c8k*fItib&`VG;|HdX@B5*)|kwEsIiu-8mAmk|QGPLD7 zJmEU*LBx}oM^Vq>K1O_v`5fKwJ>KUMkPwj(k`j{>loXW}mKK*+m{6Hfno^rnoK&4v zp8jZkeje-d^QZc}0R + \ No newline at end of file diff --git a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ComposableResource.android.kt b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ComposableResource.android.kt new file mode 100644 index 0000000000..01c312d1db --- /dev/null +++ b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ComposableResource.android.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.resources + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap + +@ExperimentalResourceApi +internal actual fun ByteArray.toImageBitmap(): ImageBitmap = toAndroidBitmap().asImageBitmap() + +private fun ByteArray.toAndroidBitmap(): Bitmap { + return BitmapFactory.decodeByteArray(this, 0, size); +} diff --git a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/Resource.android.kt b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/Resource.android.kt new file mode 100644 index 0000000000..18190a95e4 --- /dev/null +++ b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/Resource.android.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.resources + +import java.io.IOException + +@ExperimentalResourceApi +actual fun resource(path: String): Resource = AndroidResourceImpl(path) + +@ExperimentalResourceApi +private class AndroidResourceImpl(path: String) : AbstractResourceImpl(path) { + override suspend fun readBytes(): ByteArray { + val resource = (::AndroidResourceImpl.javaClass.classLoader).getResourceAsStream(path) + if (resource != null) { + return resource.readBytes() + } else { + throw MissingResourceException(path) + } + } +} + +internal actual class MissingResourceException actual constructor(path: String) : + IOException("Missing resource with path: $path") diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ComposeResource.common.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ComposeResource.common.kt new file mode 100644 index 0000000000..f39c1fcde1 --- /dev/null +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ComposeResource.common.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.resources + +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.ImageBitmap + +private val emptyImageBitmap: ImageBitmap by lazy { ImageBitmap(1, 1) } + +/** + * Get and remember resource. While loading and if resource not exists result will be null. + */ +@ExperimentalResourceApi +@Composable +fun Resource.rememberImageBitmap(): LoadState { + val state: MutableState> = remember(this) { mutableStateOf(LoadState.Loading()) } + LaunchedEffect(this) { + state.value = try { + LoadState.Success(readBytes().toImageBitmap()) + } catch (e: Exception) { + LoadState.Error(e) + } + } + return state.value +} + +/** + * return current ImageBitmap or return empty while loading + */ +@ExperimentalResourceApi +fun LoadState.orEmpty(): ImageBitmap = when (this) { + is LoadState.Loading -> emptyImageBitmap + is LoadState.Success -> this.value + is LoadState.Error -> emptyImageBitmap +} + +internal expect fun ByteArray.toImageBitmap(): ImageBitmap diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ExperimentalResourceApi.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ExperimentalResourceApi.kt new file mode 100644 index 0000000000..eb1e8a60be --- /dev/null +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ExperimentalResourceApi.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.resources + +@RequiresOptIn("This API is experimental and is likely to change in the future.") +annotation class ExperimentalResourceApi diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.common.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.common.kt new file mode 100644 index 0000000000..59f4463e1d --- /dev/null +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.common.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.resources + +/** + * Should implement equals() and hashCode() + */ +@ExperimentalResourceApi +interface Resource { + suspend fun readBytes(): ByteArray //todo in future use streaming +} + +/** + * Get a resource from /resources (for example, from commonMain/resources). + */ +@ExperimentalResourceApi +expect fun resource(path: String): Resource + +internal expect class MissingResourceException(path: String) + +@OptIn(ExperimentalResourceApi::class) +internal abstract class AbstractResourceImpl(val path: String) : Resource { + override fun equals(other: Any?): Boolean { + if (this === other) return true + return if (other is AbstractResourceImpl) { + path == other.path + } else { + false + } + } + + override fun hashCode(): Int { + return path.hashCode() + } +} diff --git a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ResourceTest.kt b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ResourceTest.kt new file mode 100644 index 0000000000..6d47e32c03 --- /dev/null +++ b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ResourceTest.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.resources + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +@OptIn(ExperimentalResourceApi::class) +class ResourceTest { + @Test + fun testResourceEquals() { + assertEquals(resource("a"), resource("a")) + } + + @Test + fun testResourceNotEquals() { + assertNotEquals(resource("a"), resource("b")) + } +} diff --git a/components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/Resource.desktop.kt b/components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/Resource.desktop.kt new file mode 100644 index 0000000000..b6379f63e3 --- /dev/null +++ b/components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/Resource.desktop.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.resources + +import java.io.IOException + +@ExperimentalResourceApi +actual fun resource(path: String): Resource = DesktopResourceImpl(path) + +@ExperimentalResourceApi +private class DesktopResourceImpl(path: String) : AbstractResourceImpl(path) { + override suspend fun readBytes(): ByteArray { + val resource = (::DesktopResourceImpl.javaClass.classLoader).getResourceAsStream(path) + if (resource != null) { + return resource.readBytes() + } else { + throw MissingResourceException(path) + } + } +} + +internal actual class MissingResourceException actual constructor(path: String) : + IOException("Missing resource with path: $path") diff --git a/components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt b/components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt new file mode 100644 index 0000000000..2803875019 --- /dev/null +++ b/components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt @@ -0,0 +1,61 @@ +package org.jetbrains.compose.resources + +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.test.junit4.createComposeRule +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test + +@OptIn(ExperimentalResourceApi::class, ExperimentalCoroutinesApi::class) +class ComposeResourceTest { + + @get:Rule + val rule = createComposeRule() + + @Test + fun testMissingResource() = runTest (UnconfinedTestDispatcher()) { + var recompositionCount = 0 + rule.setContent { + CountRecompositions(resource("missing.png").rememberImageBitmap().orEmpty()) { + recompositionCount++ + } + } + rule.awaitIdle() + assertEquals(2, recompositionCount) + } + + @Test + fun testCountRecompositions() = runTest (UnconfinedTestDispatcher()) { + val mutableStateFlow = MutableStateFlow(true) + var recompositionCount = 0 + rule.setContent { + val state: Boolean by mutableStateFlow.collectAsState(true) + val resource = resource(if (state) "1.png" else "2.png") + CountRecompositions(resource.rememberImageBitmap().orEmpty()) { + recompositionCount++ + } + } + rule.awaitIdle() + mutableStateFlow.value = false + rule.awaitIdle() + assertEquals(4, recompositionCount) + } + +} + +@Composable +private fun CountRecompositions(imageBitmap: ImageBitmap?, onRecomposition: () -> Unit) { + onRecomposition() + if (imageBitmap != null) { + Image(bitmap = imageBitmap, contentDescription = null) + } +} diff --git a/components/resources/library/src/desktopTest/resources/1.png b/components/resources/library/src/desktopTest/resources/1.png new file mode 100644 index 0000000000000000000000000000000000000000..c11cb2ec658905996d5a1ecb86e8d4c6caf106b0 GIT binary patch literal 946 zcmV;j15NyiP)^G+oiJ=!Nz8&w9k}QkJa>-xI6jWIedCSZHLW4Loc(E*m zJS!k$+4{cW5f_hdmhCXZ(zT4Q{xNYoHZFx0OLl~4{UIO;0e=v!BbKPl$nf*Ol9aE8 zDHu^^Nw&B*y!-)43JeS3o-LN*0MR-oX~&4x0ZZW0G3@;FaAT9;n5D!8UBptC3rfu5@q#Etq|B$Ut^Nkn<+$PLOVF$acHl1%cq-GaU3bAv!Lq5?R zE_NZJv`cE*+W+6jW`{fElijg{jjz1}ZBpWCpE=I4yFnJ{2D@|ZQ;J6__O;8d3O0wx zf*oR0(Js#RNyW7?>?1Zg2FQX9$YJxSJbO(Vv`?HT*jynobcM}{_E|Lr1w^TaS&4Tf zhTbV*Rzs8uByLZ;=eM~)V(J15?K{FAi3{gEd>pz+Om%VQJdnfmufQVAnn;W_F^kf_ zB70DV3@|%GV(bjFKzZ(g#62rR)-k(9V(b>Py7JsJiR&pt7BPE5V(baCqVimi#Eq39 zUCf3^j14h!mFLDJPL#GWn;2(MzZg-B z@J@~gc2oEP9E2Q&U4~qTJ&1S`^C;?B+{cKoF`uJ7-{VmjP*PM@SXx|O zVM1j_X-aKQaZ+_wd0KtmfPx7PBPwQe{tqdoXGqDEmNB*2ndbCf9dZBw00000E@A-m U1WWIxpa1{>07*qoM6N<$f)}-`i~s-t literal 0 HcmV?d00001 diff --git a/components/resources/library/src/desktopTest/resources/2.png b/components/resources/library/src/desktopTest/resources/2.png new file mode 100644 index 0000000000000000000000000000000000000000..5fa326654ef90ee5b1db23fdf4b1f21dcd6fc9f5 GIT binary patch literal 785 zcmV+s1Md8ZP)zs)waNAO|nnWVjJc)+h^E)8qz>IiW1&#F$jelb|l-ofR=5J=#>sk)ukR9w9L+ z-cc8lkSC?;JfY0tnTRfzbcuLInZpxxQmDnNdFV2y!%uXWo2L@5v`Co;-i#j~Q{jJ9 zh>e}_#sewRGJbwSjo+w|7`vcFO4JR5e@CBd=(96SggX9_H!OXRl)08NkA~$ik9P@5 zz&9irpW`H(HY+K80E}8?4)EP4@7qr&Y7Xx}(-)8=u-*HAk#FYfV=1sj5|T#yX`X zhfg_C&%FcgQ{cV`-nYSj?c8k*fItib&`VG;|HdX@B5*)|kwEsIiu-8mAmk|QGPLD7 zJmEU*LBx}oM^Vq>K1O_v`5fKwJ>KUMkPwj(k`j{>loXW}mKK*+m{6Hfno^rnoK&4v zp8jZkeje-d^QZc}0R + val req = XMLHttpRequest() + req.open("GET", "/$path", true) + req.responseType = XMLHttpRequestResponseType.ARRAYBUFFER + + req.onload = { event -> + val arrayBuffer = req.response + if (arrayBuffer is ArrayBuffer) { + continuation.resume(arrayBuffer.toByteArray()) + } else { + continuation.resumeWithException(MissingResourceException(path)) + } + } + req.send(null) + } + } +} + +private fun ArrayBuffer.toByteArray() = Int8Array(this, 0, byteLength).unsafeCast() + +internal actual class MissingResourceException actual constructor(path: String) : + Exception("Missing resource with path: $path") diff --git a/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/Resource.macos.kt b/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/Resource.macos.kt new file mode 100644 index 0000000000..fedc16c725 --- /dev/null +++ b/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/Resource.macos.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.resources + +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.usePinned +import platform.Foundation.NSData +import platform.Foundation.NSFileManager +import platform.posix.memcpy + +@ExperimentalResourceApi +actual fun resource(path: String): Resource = MacOSResourceImpl(path) + +@ExperimentalResourceApi +private class MacOSResourceImpl(path: String) : AbstractResourceImpl(path) { + override suspend fun readBytes(): ByteArray { + val currentDirectoryPath = NSFileManager.defaultManager().currentDirectoryPath + val contentsAtPath: NSData? = NSFileManager.defaultManager().run { + //todo in future bundle resources with app and use all sourceSets (skikoMain, nativeMain) + contentsAtPath("$currentDirectoryPath/src/macosMain/resources/$path") + ?: contentsAtPath("$currentDirectoryPath/src/commonMain/resources/$path") + } + if (contentsAtPath != null) { + val byteArray = ByteArray(contentsAtPath.length.toInt()) + byteArray.usePinned { + memcpy(it.addressOf(0), contentsAtPath.bytes, contentsAtPath.length) + } + return byteArray + } else { + throw MissingResourceException(path) + } + } +} + +internal actual class MissingResourceException actual constructor(path: String) : + Exception("Missing resource with path: $path") diff --git a/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/ComposeResource.skiko.kt b/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/ComposeResource.skiko.kt new file mode 100644 index 0000000000..5400b51630 --- /dev/null +++ b/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/ComposeResource.skiko.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.resources + +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.toComposeImageBitmap +import org.jetbrains.skia.Image + +internal actual fun ByteArray.toImageBitmap(): ImageBitmap = + Image.makeFromEncoded(this).toComposeImageBitmap() diff --git a/components/settings.gradle.kts b/components/settings.gradle.kts index e071ccafb8..0e9c93eb23 100644 --- a/components/settings.gradle.kts +++ b/components/settings.gradle.kts @@ -4,12 +4,14 @@ pluginManagement { mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") google() + mavenLocal() } plugins { kotlin("jvm").version(extra["kotlin.version"] as String) kotlin("multiplatform").version(extra["kotlin.version"] as String) id("org.jetbrains.compose").version(extra["compose.version"] as String) + id("com.android.library").version(extra["agp.version"] as String) } } @@ -17,4 +19,7 @@ include(":SplitPane:library") include(":SplitPane:demo") include(":AnimatedImage:library") include("AnimatedImage:demo") -include("resources:library") +include(":resources:library") +include(":resources:demo:androidApp") +include(":resources:demo:desktopApp") +include(":resources:demo:shared") diff --git a/components/test.sh b/components/test.sh new file mode 100755 index 0000000000..a16a91fcc0 --- /dev/null +++ b/components/test.sh @@ -0,0 +1,11 @@ +#!/bin/bash +cd "$(dirname "$0")" # Run always in current dir +set -euo pipefail # Fail fast + +# Unit tests +./gradlew :resources:library:test +./gradlew :resources:library:desktopTest + +# Android integration tests +./gradlew :resources:library:pixel5DebugAndroidTest + diff --git a/compose/README.md b/compose/README.md index 48accf680c..932c396d03 100644 --- a/compose/README.md +++ b/compose/README.md @@ -88,6 +88,7 @@ Run native macos sample: export COMPOSE_CUSTOM_VERSION=0.0.0-custom-version &&\ ./scripts/publishToMavenLocal -Pcompose.platforms=all &&\ ./scripts/publishGradlePluginToMavenLocal &&\ +./scripts/publishComponentsToMavenLocal &&\ ./scripts/publishWebComponentsToMavenLocal ``` `-Pcompose.platforms=all` could be replace with comma-separated list of platforms, such as `js,jvm,androidDebug,androidRelease,macosx64,uikit`. diff --git a/compose/scripts/publishComponentsToMavenLocal b/compose/scripts/publishComponentsToMavenLocal new file mode 100755 index 0000000000..34e97cc777 --- /dev/null +++ b/compose/scripts/publishComponentsToMavenLocal @@ -0,0 +1,13 @@ +#!/bin/bash + +cd "$(dirname "$0")" + +if [[ -z "$COMPOSE_CUSTOM_VERSION" ]]; then + echo "Must provide COMPOSE_CUSTOM_VERSION in environment" 1>&2 + exit 1 +fi + +pushd ../../components +./gradlew publishToMavenLocal -Pcompose.version="$COMPOSE_CUSTOM_VERSION" || exit 1 +popd + diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/configureTaskToGenerateXcodeProject.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/configureTaskToGenerateXcodeProject.kt index 4cb618cec9..5f55f5c8ba 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/configureTaskToGenerateXcodeProject.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/configureTaskToGenerateXcodeProject.kt @@ -18,6 +18,9 @@ internal fun Project.configureTaskToGenerateXcodeProject( ): TaskProvider = tasks.composeIosTask("iosGenerateXcodeProject$id") { dependsOn(taskInstallXcodeGen) doLast { + val commonMainResources = file("src/commonMain/resources").absolutePath + val uikitMainResources = file("src/uikitMain/resources").absolutePath + val iosMainResources = file("src/iosMain/resources").absolutePath val buildIosDir = getBuildIosDir(id) buildIosDir.mkdirs() buildIosDir.resolve("project.yml").writeText( @@ -44,6 +47,16 @@ internal fun Project.configureTaskToGenerateXcodeProject( ENABLE_BITCODE: "YES" ONLY_ACTIVE_ARCH: "NO" VALID_ARCHS: "arm64" + sources: + - path: $commonMainResources + optional: true + buildPhase: resources + - path: $uikitMainResources + optional: true + buildPhase: resources + - path: $iosMainResources + optional: true + buildPhase: resources """.trimIndent() ) runExternalTool(xcodeGenExecutable, emptyList(), workingDir = buildIosDir)