From afe548b6d9264d76b158b864fda4101e189dcc56 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Tue, 23 Apr 2024 18:20:22 +0200 Subject: [PATCH] [resources] Add functions to retrieve bytes from drawable or font resources. (#4651) Implemented two new experimental functions: ```kotlin /** * Retrieves the byte array of the drawable resource. * * @param environment The resource environment, which can be obtained from [rememberResourceEnvironment] or [getSystemResourceEnvironment]. * @param resource The drawable resource. * @return The byte array representing the drawable resource. */ @ExperimentalResourceApi suspend fun getDrawableResourceBytes( environment: ResourceEnvironment, resource: DrawableResource ): ByteArray {...} /** * Retrieves the byte array of the font resource. * * @param environment The resource environment, which can be obtained from [rememberResourceEnvironment] or [getSystemResourceEnvironment]. * @param resource The font resource. * @return The byte array representing the font resource. */ @ExperimentalResourceApi suspend fun getFontResourceBytes( environment: ResourceEnvironment, resource: FontResource ): ByteArray {...} ``` fixes https://github.com/JetBrains/compose-multiplatform/issues/4360 --- components/README.md | 4 -- components/gradle.properties | 2 +- .../resources/demo/shared/build.gradle.kts | 16 ------ .../compose/resources/demo/shared/FileRes.kt | 53 ++++++++++++++++- .../composeResources/files/platform-text.txt | 1 - .../shared/src/macosMain/kotlin/main.macos.kt | 17 ------ components/resources/library/build.gradle.kts | 1 + .../compose/resources/FontResources.kt | 19 ++++++- .../compose/resources/ImageResources.kt | 17 +++++- .../resources/PluralStringResources.kt | 45 ++++++++++++++- .../compose/resources/ResourceEnvironment.kt | 57 ++++++++++++++++--- .../compose/resources/StringArrayResources.kt | 17 +++++- .../compose/resources/StringResources.kt | 39 ++++++++++++- .../compose/resources/ComposeResourceTest.kt | 24 +++++++- .../jetbrains/compose/resources/TestUtils.kt | 5 ++ 15 files changed, 261 insertions(+), 56 deletions(-) delete mode 100644 components/resources/demo/shared/src/macosMain/composeResources/files/platform-text.txt delete mode 100644 components/resources/demo/shared/src/macosMain/kotlin/main.macos.kt diff --git a/components/README.md b/components/README.md index 7c58b9785b..7ec4fc353c 100644 --- a/components/README.md +++ b/components/README.md @@ -13,10 +13,6 @@ in Android Studio or in AppCode with [installed CocoaPods](https://kotlinlang.or ### 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 diff --git a/components/gradle.properties b/components/gradle.properties index f44311c992..9b1f014bb7 100644 --- a/components/gradle.properties +++ b/components/gradle.properties @@ -8,7 +8,7 @@ android.useAndroidX=true #Versions kotlin.version=1.9.23 -compose.version=1.6.10-beta01 +compose.version=1.6.10-dev1596 agp.version=8.2.2 #Compose diff --git a/components/resources/demo/shared/build.gradle.kts b/components/resources/demo/shared/build.gradle.kts index cecd7b3f1d..cc87a66ef0 100644 --- a/components/resources/demo/shared/build.gradle.kts +++ b/components/resources/demo/shared/build.gradle.kts @@ -39,24 +39,8 @@ kotlin { binaries.executable() } - listOf( - macosX64(), - macosArm64() - ).forEach { macosTarget -> - macosTarget.binaries { - executable { - entryPoint = "main" - } - } - } - applyDefaultHierarchyTemplate() sourceSets { - all { - languageSettings { - optIn("org.jetbrains.compose.resources.ExperimentalResourceApi") - } - } val desktopMain by getting val wasmJsMain by getting diff --git a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt index efe4b68737..fc40f20c2f 100644 --- a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt +++ b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt @@ -9,11 +9,16 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import components.resources.demo.shared.generated.resources.Res +import components.resources.demo.shared.generated.resources.droid_icon +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.getDrawableResourceBytes +import org.jetbrains.compose.resources.rememberResourceEnvironment +@OptIn(ExperimentalResourceApi::class) @Composable fun FileRes(paddingValues: PaddingValues) { Column( - modifier = Modifier.padding(paddingValues) + modifier = Modifier.padding(paddingValues).verticalScroll(rememberScrollState()) ) { Text( modifier = Modifier.padding(16.dp), @@ -48,6 +53,34 @@ fun FileRes(paddingValues: PaddingValues) { Text(bytes.decodeToString()) """.trimIndent() ) + HorizontalDivider(modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp)) + OutlinedCard( + modifier = Modifier.padding(horizontal = 16.dp), + shape = RoundedCornerShape(4.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer) + ) { + val composeEnv = rememberResourceEnvironment() + var bytes by remember { mutableStateOf(ByteArray(0)) } + LaunchedEffect(Unit) { + bytes = getDrawableResourceBytes(composeEnv, Res.drawable.droid_icon) + } + Text( + modifier = Modifier.padding(8.dp), + text = "droid_icon byte size = " + bytes.size, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + Text( + modifier = Modifier.padding(16.dp), + text = """ + val composeEnv = rememberResourceEnvironment() + var bytes by remember { mutableStateOf(ByteArray(0)) } + LaunchedEffect(Unit) { + bytes = getDrawableResourceBytes(composeEnv, Res.drawable.droid_icon) + } + Text("droid_icon byte size = " + bytes.size) + """.trimIndent() + ) Text( modifier = Modifier.padding(16.dp), text = "File: 'files/platform-text.txt'", @@ -80,5 +113,23 @@ fun FileRes(paddingValues: PaddingValues) { Text(bytes.decodeToString()) """.trimIndent() ) + HorizontalDivider(modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp)) + OutlinedCard( + modifier = Modifier.padding(horizontal = 16.dp), + shape = RoundedCornerShape(4.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer) + ) { + Text( + modifier = Modifier.padding(8.dp), + text = "File URI: " + Res.getUri("files/platform-text.txt"), + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + Text( + modifier = Modifier.padding(16.dp), + text = """ + Text("File URI: " + Res.getUri("files/platform-text.txt")) + """.trimIndent() + ) } } \ No newline at end of file diff --git a/components/resources/demo/shared/src/macosMain/composeResources/files/platform-text.txt b/components/resources/demo/shared/src/macosMain/composeResources/files/platform-text.txt deleted file mode 100644 index c0ab602c16..0000000000 --- a/components/resources/demo/shared/src/macosMain/composeResources/files/platform-text.txt +++ /dev/null @@ -1 +0,0 @@ -macOS platform \ 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 deleted file mode 100644 index 963d3badf7..0000000000 --- a/components/resources/demo/shared/src/macosMain/kotlin/main.macos.kt +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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 6cb66172d9..b23ba687eb 100644 --- a/components/resources/library/build.gradle.kts +++ b/components/resources/library/build.gradle.kts @@ -53,6 +53,7 @@ kotlin { optIn("kotlinx.cinterop.ExperimentalForeignApi") optIn("kotlin.experimental.ExperimentalNativeApi") optIn("org.jetbrains.compose.resources.InternalResourceApi") + optIn("org.jetbrains.compose.resources.ExperimentalResourceApi") } } diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt index 1493218d58..b39b2db4e3 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt @@ -2,6 +2,7 @@ package org.jetbrains.compose.resources import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.runtime.getValue import androidx.compose.ui.text.font.* /** @@ -32,4 +33,20 @@ expect fun Font( resource: FontResource, weight: FontWeight = FontWeight.Normal, style: FontStyle = FontStyle.Normal -): Font \ No newline at end of file +): Font + +/** + * Retrieves the byte array of the font resource. + * + * @param environment The resource environment, which can be obtained from [rememberResourceEnvironment] or [getSystemResourceEnvironment]. + * @param resource The font resource. + * @return The byte array representing the font resource. + */ +@ExperimentalResourceApi +suspend fun getFontResourceBytes( + environment: ResourceEnvironment, + resource: FontResource +): ByteArray { + val resourceItem = resource.getResourceItemByEnvironment(environment) + return DefaultResourceReader.read(resourceItem.path) +} \ No newline at end of file diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt index cdb9d85224..8b26b6f5ad 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt @@ -95,7 +95,6 @@ internal expect fun SvgElement.toSvgPainter(density: Density): Painter private val emptySvgPainter: Painter by lazy { BitmapPainter(emptyImageBitmap) } -@OptIn(ExperimentalResourceApi::class) @Composable private fun svgPainter(resource: DrawableResource): Painter { val resourceReader = LocalResourceReader.current @@ -110,6 +109,22 @@ private fun svgPainter(resource: DrawableResource): Painter { return svgPainter } +/** + * Retrieves the byte array of the drawable resource. + * + * @param environment The resource environment, which can be obtained from [rememberResourceEnvironment] or [getSystemResourceEnvironment]. + * @param resource The drawable resource. + * @return The byte array representing the drawable resource. + */ +@ExperimentalResourceApi +suspend fun getDrawableResourceBytes( + environment: ResourceEnvironment, + resource: DrawableResource +): ByteArray { + val resourceItem = resource.getResourceItemByEnvironment(environment) + return DefaultResourceReader.read(resourceItem.path) +} + internal expect fun ByteArray.toImageBitmap(): ImageBitmap internal expect fun ByteArray.toXmlElement(): Element internal expect fun ByteArray.toSvgElement(): SvgElement diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PluralStringResources.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PluralStringResources.kt index abe769000f..27be9337f5 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PluralStringResources.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PluralStringResources.kt @@ -43,7 +43,24 @@ fun pluralStringResource(resource: PluralStringResource, quantity: Int): String * @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file. */ suspend fun getPluralString(resource: PluralStringResource, quantity: Int): String = - loadPluralString(resource, quantity, DefaultResourceReader, getResourceEnvironment()) + loadPluralString(resource, quantity, DefaultResourceReader, getSystemResourceEnvironment()) + +/** + * Loads a string using the specified string resource. + * + * @param environment The resource environment. + * @param resource The string resource to be used. + * @param quantity The quantity of the pluralization to use. + * @return The loaded string resource. + * + * @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file. + */ +@ExperimentalResourceApi +suspend fun getPluralString( + environment: ResourceEnvironment, + resource: PluralStringResource, + quantity: Int +): String = loadPluralString(resource, quantity, DefaultResourceReader, environment) private suspend fun loadPluralString( resource: PluralStringResource, @@ -99,9 +116,33 @@ suspend fun getPluralString(resource: PluralStringResource, quantity: Int, varar resource, quantity, formatArgs.map { it.toString() }, DefaultResourceReader, - getResourceEnvironment(), + getSystemResourceEnvironment(), ) +/** + * Loads a string using the specified string resource. + * + * @param environment The resource environment. + * @param resource The string resource to be used. + * @param quantity The quantity of the pluralization to use. + * @param formatArgs The arguments to be inserted into the formatted string. + * @return The loaded string resource. + * + * @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file. + */ +@ExperimentalResourceApi +suspend fun getPluralString( + environment: ResourceEnvironment, + resource: PluralStringResource, + quantity: Int, + vararg formatArgs: Any +): String = loadPluralString( + resource, quantity, + formatArgs.map { it.toString() }, + DefaultResourceReader, + environment +) + private suspend fun loadPluralString( resource: PluralStringResource, quantity: Int, diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.kt index 51b37b8313..20ac17ca09 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.kt @@ -5,12 +5,35 @@ import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.intl.Locale -internal data class ResourceEnvironment( - val language: LanguageQualifier, - val region: RegionQualifier, - val theme: ThemeQualifier, - val density: DensityQualifier -) +@ExperimentalResourceApi +class ResourceEnvironment internal constructor( + internal val language: LanguageQualifier, + internal val region: RegionQualifier, + internal val theme: ThemeQualifier, + internal val density: DensityQualifier +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as ResourceEnvironment + + if (language != other.language) return false + if (region != other.region) return false + if (theme != other.theme) return false + if (density != other.density) return false + + return true + } + + override fun hashCode(): Int { + var result = language.hashCode() + result = 31 * result + region.hashCode() + result = 31 * result + theme.hashCode() + result = 31 * result + density.hashCode() + return result + } +} internal interface ComposeEnvironment { @Composable @@ -39,14 +62,32 @@ internal val DefaultComposeEnvironment = object : ComposeEnvironment { //ComposeEnvironment provider will be overridden for tests internal val LocalComposeEnvironment = staticCompositionLocalOf { DefaultComposeEnvironment } +/** + * Returns an instance of [ResourceEnvironment]. + * + * The [ResourceEnvironment] class represents the environment for resources. + * + * @return An instance of [ResourceEnvironment] representing the current environment. + */ +@ExperimentalResourceApi +@Composable +fun rememberResourceEnvironment(): ResourceEnvironment { + val composeEnvironment = LocalComposeEnvironment.current + return composeEnvironment.rememberEnvironment() +} + internal expect fun getSystemEnvironment(): ResourceEnvironment //the function reference will be overridden for tests +//@TestOnly +internal var getResourceEnvironment = ::getSystemEnvironment + /** - * Provides the resource environment for non-composable access to string resources. + * Provides the resource environment for non-composable access to resources. * It is an expensive operation! Don't use it in composable functions with no cache! */ -internal var getResourceEnvironment = ::getSystemEnvironment +@ExperimentalResourceApi +fun getSystemResourceEnvironment(): ResourceEnvironment = getResourceEnvironment() @OptIn(InternalResourceApi::class) internal fun Resource.getResourceItemByEnvironment(environment: ResourceEnvironment): ResourceItem { diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringArrayResources.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringArrayResources.kt index 6eb35147e8..e89aa5c600 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringArrayResources.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringArrayResources.kt @@ -46,7 +46,22 @@ fun stringArrayResource(resource: StringArrayResource): List { * @throws IllegalStateException if the string array with the given ID is not found. */ suspend fun getStringArray(resource: StringArrayResource): List = - loadStringArray(resource, DefaultResourceReader, getResourceEnvironment()) + loadStringArray(resource, DefaultResourceReader, getSystemResourceEnvironment()) + +/** + * Loads a list of strings using the specified string array resource. + * + * @param environment The resource environment. + * @param resource The string array resource to be used. + * @return A list of strings representing the items in the string array. + * + * @throws IllegalStateException if the string array with the given ID is not found. + */ +@ExperimentalResourceApi +suspend fun getStringArray( + environment: ResourceEnvironment, + resource: StringArrayResource +): List = loadStringArray(resource, DefaultResourceReader, environment) private suspend fun loadStringArray( resource: StringArrayResource, diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt index 9fcbc4c9b9..3cce39b722 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt @@ -39,7 +39,20 @@ fun stringResource(resource: StringResource): String { * @throws IllegalArgumentException If the provided ID is not found in the resource file. */ suspend fun getString(resource: StringResource): String = - loadString(resource, DefaultResourceReader, getResourceEnvironment()) + loadString(resource, DefaultResourceReader, getSystemResourceEnvironment()) + +/** + * Loads a string using the specified string resource. + * + * @param environment The resource environment. + * @param resource The string resource to be used. + * @return The loaded string resource. + * + * @throws IllegalArgumentException If the provided ID is not found in the resource file. + */ +@ExperimentalResourceApi +suspend fun getString(environment: ResourceEnvironment, resource: StringResource): String = + loadString(resource, DefaultResourceReader, environment) private suspend fun loadString( resource: StringResource, @@ -83,7 +96,29 @@ suspend fun getString(resource: StringResource, vararg formatArgs: Any): String resource, formatArgs.map { it.toString() }, DefaultResourceReader, - getResourceEnvironment() + getSystemResourceEnvironment() +) + +/** + * Loads a formatted string using the specified string resource and arguments. + * + * @param environment The resource environment. + * @param resource The string resource to be used. + * @param formatArgs The arguments to be inserted into the formatted string. + * @return The formatted string resource. + * + * @throws IllegalArgumentException If the provided ID is not found in the resource file. + */ +@ExperimentalResourceApi +suspend fun getString( + environment: ResourceEnvironment, + resource: StringResource, + vararg formatArgs: Any +): String = loadString( + resource, + formatArgs.map { it.toString() }, + DefaultResourceReader, + environment ) private suspend fun loadString( diff --git a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt index d12ececcda..6e191a944c 100644 --- a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt +++ b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.* import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.runComposeUiTest import kotlinx.coroutines.test.runTest -import org.jetbrains.skiko.URIManager import kotlin.test.* @OptIn(ExperimentalTestApi::class, InternalResourceApi::class) @@ -304,4 +303,27 @@ class ComposeResourceTest { assertTrue(uri1.endsWith("/1.png")) assertTrue(uri2.endsWith("/2.png")) } + + @OptIn(ExperimentalResourceApi::class) + @Test + fun testGetResourceBytes() = runTest { + val env = getSystemEnvironment() + val imageBytes = getDrawableResourceBytes(env, TestDrawableResource("1.png")) + assertEquals(946, imageBytes.size) + val fontBytes = getFontResourceBytes(env, TestFontResource("font_awesome.otf")) + assertEquals(134808, fontBytes.size) + } + + @OptIn(ExperimentalResourceApi::class) + @Test + fun testGetResourceEnvironment() = runComposeUiTest { + var environment: ResourceEnvironment? = null + setContent { + environment = rememberResourceEnvironment() + } + waitForIdle() + + val systemEnvironment = getSystemEnvironment() + assertEquals(systemEnvironment, environment) + } } diff --git a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt index e260331f78..42f5b4f02c 100644 --- a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt +++ b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt @@ -40,6 +40,11 @@ internal fun TestDrawableResource(path: String) = DrawableResource( setOf(ResourceItem(emptySet(), path, -1, -1)) ) +internal fun TestFontResource(path: String) = FontResource( + path, + setOf(ResourceItem(emptySet(), path, -1, -1)) +) + internal fun parsePluralSamples(samples: String): List { return samples.split(',').flatMap { val range = it.trim()