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 0000000000..f33b6c2622 Binary files /dev/null and b/components/resources/demo/shared/src/commonMain/resources/dir/img.png differ 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 0000000000..0da983e2ce Binary files /dev/null and b/components/resources/demo/shared/src/commonMain/resources/img.webp differ 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 0000000000..c11cb2ec65 Binary files /dev/null and b/components/resources/library/src/androidAndroidTest/resources/1.png differ 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 0000000000..5fa326654e Binary files /dev/null and b/components/resources/library/src/androidAndroidTest/resources/2.png differ diff --git a/components/resources/library/src/androidMain/AndroidManifest.xml b/components/resources/library/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000000..8cbd70ab54 --- /dev/null +++ b/components/resources/library/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ 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 0000000000..c11cb2ec65 Binary files /dev/null and b/components/resources/library/src/desktopTest/resources/1.png differ 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 0000000000..5fa326654e Binary files /dev/null and b/components/resources/library/src/desktopTest/resources/2.png differ diff --git a/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/Resource.ios.kt b/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/Resource.ios.kt new file mode 100644 index 0000000000..c081555e1b --- /dev/null +++ b/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/Resource.ios.kt @@ -0,0 +1,37 @@ +/* + * 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.NSBundle +import platform.Foundation.NSData +import platform.Foundation.NSFileManager +import platform.posix.memcpy + +@ExperimentalResourceApi +actual fun resource(path: String): Resource = UIKitResourceImpl(path) + +@ExperimentalResourceApi +private class UIKitResourceImpl(path: String) : AbstractResourceImpl(path) { + override suspend fun readBytes(): ByteArray { + val absolutePath = NSBundle.mainBundle.resourcePath + "/" + path + val contentsAtPath: NSData? = NSFileManager.defaultManager().contentsAtPath(absolutePath) + 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/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt new file mode 100644 index 0000000000..e0a78a5193 --- /dev/null +++ b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt @@ -0,0 +1,44 @@ +/* + * 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 org.khronos.webgl.ArrayBuffer +import org.khronos.webgl.Int8Array +import org.w3c.xhr.ARRAYBUFFER +import org.w3c.xhr.XMLHttpRequest +import org.w3c.xhr.XMLHttpRequestResponseType +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +@ExperimentalResourceApi +actual fun resource(path: String): Resource = JSResourceImpl(path) + +@ExperimentalResourceApi +private class JSResourceImpl(path: String) : AbstractResourceImpl(path) { + override suspend fun readBytes(): ByteArray { + return suspendCoroutine { continuation -> + 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)