From 27915cbc0fd1e2ca801d83fc0309f8deba5e31de Mon Sep 17 00:00:00 2001 From: Konstantin Date: Wed, 20 Dec 2023 19:24:21 +0100 Subject: [PATCH] Get environment and select resource by qualifiers (#4018) --- .../compose/resources/demo/shared/FileRes.kt | 6 +- .../compose/resources/demo/shared/FontRes.kt | 2 +- .../resources/demo/shared/ImagesRes.kt | 32 ++--- .../resources/demo/shared/StringRes.kt | 24 ++-- .../{images => drawable}/compose.png | Bin .../{images => drawable}/droid_icon.xml | 0 .../{images => drawable}/insta_icon.xml | 0 .../composeRes/{images => drawable}/land.webp | Bin .../{fonts => font}/font_awesome.otf | Bin .../resources/ComposeResourceTest.android.kt | 63 +++++++-- .../resources/FontResources.android.kt | 8 +- .../resources/ResourceEnvironment.android.kt | 18 +++ .../resources/PlatformState.blocking.kt | 14 -- .../resources/ResourceState.blocking.kt | 18 +++ .../compose/resources/FontResources.kt | 4 +- .../compose/resources/ImageResources.kt | 55 ++++---- .../jetbrains/compose/resources/Qualifier.kt | 50 ++++++++ .../jetbrains/compose/resources/Resource.kt | 7 +- .../compose/resources/ResourceEnvironment.kt | 73 +++++++++++ .../{PlatformState.kt => ResourceState.kt} | 4 +- .../compose/resources/StringResources.kt | 67 ++++++---- .../compose/resources/ResourceTest.kt | 120 ++++++++++++------ .../resources/ResourceEnvironment.desktop.kt | 19 +++ .../resources/ComposeResourceTest.desktop.kt | 67 ++++++++-- .../resources/ResourceEnvironment.ios.kt | 21 +++ .../compose/resources/ImageResources.js.kt | 4 +- .../resources/ResourceEnvironment.js.kt | 23 ++++ .../resources/ResourceEnvironment.macos.kt | 32 +++++ .../compose/resources/FontResources.skiko.kt | 8 +- .../resources/ResourceEnvironment.wasmJs.kt | 23 ++++ ...tformState.web.kt => ResourceState.web.kt} | 9 +- .../resources/AndroidTargetConfiguration.kt | 2 +- .../compose/resources/GenerateResClassTask.kt | 4 +- .../compose/resources/ResourcesSpec.kt | 105 +++++++++++---- .../test/tests/integration/ResourcesTest.kt | 54 +++++++- .../misc/commonResources/expected/Res.kt | 99 ++++++++++----- .../vector.xml | 0 .../vector.xml | 0 .../{images-q2 => drawable-en}/vector.xml | 0 .../{images => drawable}/3-strange-name.xml | 0 .../{images => drawable}/vector.xml | 0 .../{images => drawable}/vector_2.xml | 0 .../composeRes/{fonts => font}/emptyFont.otf | Bin 43 files changed, 784 insertions(+), 251 deletions(-) rename components/resources/demo/shared/src/commonMain/resources/composeRes/{images => drawable}/compose.png (100%) rename components/resources/demo/shared/src/commonMain/resources/composeRes/{images => drawable}/droid_icon.xml (100%) rename components/resources/demo/shared/src/commonMain/resources/composeRes/{images => drawable}/insta_icon.xml (100%) rename components/resources/demo/shared/src/commonMain/resources/composeRes/{images => drawable}/land.webp (100%) rename components/resources/demo/shared/src/commonMain/resources/composeRes/{fonts => font}/font_awesome.otf (100%) create mode 100644 components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.android.kt delete mode 100644 components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/PlatformState.blocking.kt create mode 100644 components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/ResourceState.blocking.kt create mode 100644 components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Qualifier.kt create mode 100644 components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.kt rename components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/{PlatformState.kt => ResourceState.kt} (80%) create mode 100644 components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.desktop.kt create mode 100644 components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.ios.kt create mode 100644 components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.js.kt create mode 100644 components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.macos.kt create mode 100644 components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.wasmJs.kt rename components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/{PlatformState.web.kt => ResourceState.web.kt} (62%) rename gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/{images-q1-q2 => drawable-au-rUS}/vector.xml (100%) rename gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/{images-q1 => drawable-dark-ge}/vector.xml (100%) rename gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/{images-q2 => drawable-en}/vector.xml (100%) rename gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/{images => drawable}/3-strange-name.xml (100%) rename gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/{images => drawable}/vector.xml (100%) rename gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/{images => drawable}/vector_2.xml (100%) rename gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/{fonts => font}/emptyFont.otf (100%) 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 f7a3877432..298c280d52 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 @@ -28,7 +28,7 @@ fun FileRes(paddingValues: PaddingValues) { ) { Text( modifier = Modifier.padding(16.dp), - text = "File: 'composeRes/images/droid_icon.xml'", + text = "File: 'composeRes/drawable/droid_icon.xml'", style = MaterialTheme.typography.titleLarge ) OutlinedCard( @@ -38,7 +38,7 @@ fun FileRes(paddingValues: PaddingValues) { ) { var bytes by remember { mutableStateOf(ByteArray(0)) } LaunchedEffect(Unit) { - bytes = readResourceBytes("composeRes/images/droid_icon.xml") + bytes = readResourceBytes("composeRes/drawable/droid_icon.xml") } Text( modifier = Modifier.padding(8.dp).height(200.dp).verticalScroll(rememberScrollState()), @@ -54,7 +54,7 @@ fun FileRes(paddingValues: PaddingValues) { mutableStateOf(ByteArray(0)) } LaunchedEffect(Unit) { - bytes = readBytes("composeRes/images/droid_icon.xml") + bytes = readResourceBytes("composeRes/drawable/droid_icon.xml") } Text(bytes.decodeToString()) """.trimIndent() diff --git a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FontRes.kt b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FontRes.kt index eaedd84ce1..93deb00e23 100644 --- a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FontRes.kt +++ b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FontRes.kt @@ -43,7 +43,7 @@ fun FontRes(paddingValues: PaddingValues) { ) } - val fontAwesome = FontFamily(Font(Res.fonts.font_awesome)) + val fontAwesome = FontFamily(Font(Res.font.font_awesome)) val symbols = arrayOf(0xf1ba, 0xf238, 0xf21a, 0xf1bb, 0xf1b8, 0xf09b, 0xf269, 0xf1d0, 0xf15a, 0xf293, 0xf1c6) Text( modifier = Modifier.padding(16.dp), diff --git a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/ImagesRes.kt b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/ImagesRes.kt index fcb0d1a6db..0ef3d25891 100644 --- a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/ImagesRes.kt +++ b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/ImagesRes.kt @@ -28,13 +28,13 @@ fun ImagesRes(contentPadding: PaddingValues) { ) { Image( modifier = Modifier.size(100.dp), - painter = painterResource(Res.images.compose), + painter = painterResource(Res.drawable.compose), contentDescription = null ) Text( """ Image( - painter = painterResource(Res.images.compose) + painter = painterResource(Res.drawable.compose) ) """.trimIndent() ) @@ -47,13 +47,13 @@ fun ImagesRes(contentPadding: PaddingValues) { ) { Image( modifier = Modifier.size(100.dp), - painter = painterResource(Res.images.insta_icon), + painter = painterResource(Res.drawable.insta_icon), contentDescription = null ) Text( """ Image( - painter = painterResource(Res.images.insta_icon) + painter = painterResource(Res.drawable.insta_icon) ) """.trimIndent() ) @@ -66,13 +66,13 @@ fun ImagesRes(contentPadding: PaddingValues) { ) { Image( modifier = Modifier.size(140.dp), - bitmap = imageResource(Res.images.land), + bitmap = imageResource(Res.drawable.land), contentDescription = null ) Text( """ Image( - bitmap = imageResource(Res.images.land) + bitmap = imageResource(Res.drawable.land) ) """.trimIndent() ) @@ -85,13 +85,13 @@ fun ImagesRes(contentPadding: PaddingValues) { ) { Image( modifier = Modifier.size(100.dp), - imageVector = vectorResource(Res.images.droid_icon), + imageVector = vectorResource(Res.drawable.droid_icon), contentDescription = null ) Text( """ Image( - imageVector = vectorResource(Res.images.droid_icon) + imageVector = vectorResource(Res.drawable.droid_icon) ) """.trimIndent() ) @@ -104,13 +104,13 @@ fun ImagesRes(contentPadding: PaddingValues) { ) { Icon( modifier = Modifier.size(100.dp), - painter = painterResource(Res.images.compose), + painter = painterResource(Res.drawable.compose), contentDescription = null ) Text( """ Icon( - painter = painterResource(Res.images.compose) + painter = painterResource(Res.drawable.compose) ) """.trimIndent() ) @@ -123,13 +123,13 @@ fun ImagesRes(contentPadding: PaddingValues) { ) { Icon( modifier = Modifier.size(100.dp), - painter = painterResource(Res.images.insta_icon), + painter = painterResource(Res.drawable.insta_icon), contentDescription = null ) Text( """ Icon( - painter = painterResource(Res.images.insta_icon) + painter = painterResource(Res.drawable.insta_icon) ) """.trimIndent() ) @@ -142,13 +142,13 @@ fun ImagesRes(contentPadding: PaddingValues) { ) { Icon( modifier = Modifier.size(140.dp), - bitmap = imageResource(Res.images.land), + bitmap = imageResource(Res.drawable.land), contentDescription = null ) Text( """ Icon( - bitmap = imageResource(Res.images.land) + bitmap = imageResource(Res.drawable.land) ) """.trimIndent() ) @@ -161,13 +161,13 @@ fun ImagesRes(contentPadding: PaddingValues) { ) { Icon( modifier = Modifier.size(100.dp), - imageVector = vectorResource(Res.images.droid_icon), + imageVector = vectorResource(Res.drawable.droid_icon), contentDescription = null ) Text( """ Icon( - imageVector = vectorResource(Res.images.droid_icon) + imageVector = vectorResource(Res.drawable.droid_icon) ) """.trimIndent() ) diff --git a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt index d579ce69b4..3bcf9312ac 100644 --- a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt +++ b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt @@ -22,8 +22,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import components.resources.demo.generated.resources.Res -import org.jetbrains.compose.resources.getString -import org.jetbrains.compose.resources.getStringArray +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.resources.stringArrayResource import org.jetbrains.compose.resources.readResourceBytes @Composable @@ -54,9 +54,9 @@ fun StringRes(paddingValues: PaddingValues) { } OutlinedTextField( modifier = Modifier.padding(16.dp).fillMaxWidth(), - value = getString(Res.strings.app_name), + value = stringResource(Res.string.app_name), onValueChange = {}, - label = { Text("Text(getString(Res.strings.app_name)") }, + label = { Text("Text(stringResource(Res.string.app_name)") }, enabled = false, colors = TextFieldDefaults.colors( disabledTextColor = MaterialTheme.colorScheme.onSurface, @@ -66,9 +66,9 @@ fun StringRes(paddingValues: PaddingValues) { ) OutlinedTextField( modifier = Modifier.padding(16.dp).fillMaxWidth(), - value = getString(Res.strings.hello), + value = stringResource(Res.string.hello), onValueChange = {}, - label = { Text("Text(getString(Res.strings.hello)") }, + label = { Text("Text(stringResource(Res.string.hello)") }, enabled = false, colors = TextFieldDefaults.colors( disabledTextColor = MaterialTheme.colorScheme.onSurface, @@ -78,9 +78,9 @@ fun StringRes(paddingValues: PaddingValues) { ) OutlinedTextField( modifier = Modifier.padding(16.dp).fillMaxWidth(), - value = getString(Res.strings.multi_line), + value = stringResource(Res.string.multi_line), onValueChange = {}, - label = { Text("Text(getString(Res.strings.multi_line)") }, + label = { Text("Text(stringResource(Res.string.multi_line)") }, enabled = false, colors = TextFieldDefaults.colors( disabledTextColor = MaterialTheme.colorScheme.onSurface, @@ -90,9 +90,9 @@ fun StringRes(paddingValues: PaddingValues) { ) OutlinedTextField( modifier = Modifier.padding(16.dp).fillMaxWidth(), - value = getString(Res.strings.str_template, "User_name", 100), + value = stringResource(Res.string.str_template, "User_name", 100), onValueChange = {}, - label = { Text("Text(getString(Res.strings.str_template, \"User_name\", 100)") }, + label = { Text("Text(stringResource(Res.string.str_template, \"User_name\", 100)") }, enabled = false, colors = TextFieldDefaults.colors( disabledTextColor = MaterialTheme.colorScheme.onSurface, @@ -102,9 +102,9 @@ fun StringRes(paddingValues: PaddingValues) { ) OutlinedTextField( modifier = Modifier.padding(16.dp).fillMaxWidth(), - value = getStringArray(Res.strings.str_arr).toString(), + value = stringArrayResource(Res.string.str_arr).toString(), onValueChange = {}, - label = { Text("Text(getStringArray(Res.strings.str_arr).toString())") }, + label = { Text("Text(stringArrayResource(Res.string.str_arr).toString())") }, enabled = false, colors = TextFieldDefaults.colors( disabledTextColor = MaterialTheme.colorScheme.onSurface, diff --git a/components/resources/demo/shared/src/commonMain/resources/composeRes/images/compose.png b/components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/compose.png similarity index 100% rename from components/resources/demo/shared/src/commonMain/resources/composeRes/images/compose.png rename to components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/compose.png diff --git a/components/resources/demo/shared/src/commonMain/resources/composeRes/images/droid_icon.xml b/components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/droid_icon.xml similarity index 100% rename from components/resources/demo/shared/src/commonMain/resources/composeRes/images/droid_icon.xml rename to components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/droid_icon.xml diff --git a/components/resources/demo/shared/src/commonMain/resources/composeRes/images/insta_icon.xml b/components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/insta_icon.xml similarity index 100% rename from components/resources/demo/shared/src/commonMain/resources/composeRes/images/insta_icon.xml rename to components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/insta_icon.xml diff --git a/components/resources/demo/shared/src/commonMain/resources/composeRes/images/land.webp b/components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/land.webp similarity index 100% rename from components/resources/demo/shared/src/commonMain/resources/composeRes/images/land.webp rename to components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/land.webp diff --git a/components/resources/demo/shared/src/commonMain/resources/composeRes/fonts/font_awesome.otf b/components/resources/demo/shared/src/commonMain/resources/composeRes/font/font_awesome.otf similarity index 100% rename from components/resources/demo/shared/src/commonMain/resources/composeRes/fonts/font_awesome.otf rename to components/resources/demo/shared/src/commonMain/resources/composeRes/font/font_awesome.otf diff --git a/components/resources/library/src/androidInstrumentedTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.android.kt b/components/resources/library/src/androidInstrumentedTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.android.kt index aed5d003a4..b146f32b5b 100644 --- a/components/resources/library/src/androidInstrumentedTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.android.kt +++ b/components/resources/library/src/androidInstrumentedTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.android.kt @@ -12,6 +12,7 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith @OptIn(ExperimentalResourceApi::class, ExperimentalTestApi::class) class ComposeResourceTest { @@ -25,7 +26,7 @@ class ComposeResourceTest { @Test fun testCountRecompositions() = runComposeUiTest { runBlockingTest { - val imagePathFlow = MutableStateFlow(ImageResource("1.png")) + val imagePathFlow = MutableStateFlow(DrawableResource("1.png")) val recompositionsCounter = RecompositionsCounter() setContent { val res by imagePathFlow.collectAsState() @@ -35,7 +36,7 @@ class ComposeResourceTest { } } awaitIdle() - imagePathFlow.emit(ImageResource("2.png")) + imagePathFlow.emit(DrawableResource("2.png")) awaitIdle() assertEquals(2, recompositionsCounter.count) } @@ -45,7 +46,7 @@ class ComposeResourceTest { fun testImageResourceCache() = runComposeUiTest { runBlockingTest { val testResourceReader = TestResourceReader() - val imagePathFlow = MutableStateFlow(ImageResource("1.png")) + val imagePathFlow = MutableStateFlow(DrawableResource("1.png")) setContent { CompositionLocalProvider(LocalResourceReader provides testResourceReader) { val res by imagePathFlow.collectAsState() @@ -53,9 +54,9 @@ class ComposeResourceTest { } } awaitIdle() - imagePathFlow.emit(ImageResource("2.png")) + imagePathFlow.emit(DrawableResource("2.png")) awaitIdle() - imagePathFlow.emit(ImageResource("1.png")) + imagePathFlow.emit(DrawableResource("1.png")) awaitIdle() assertEquals( @@ -73,8 +74,8 @@ class ComposeResourceTest { setContent { CompositionLocalProvider(LocalResourceReader provides testResourceReader) { val res by stringIdFlow.collectAsState() - Text(getString(res)) - Text(getStringArray(TestStringResource("str_arr")).joinToString()) + Text(stringResource(res)) + Text(stringArrayResource(TestStringResource("str_arr")).joinToString()) } } awaitIdle() @@ -94,14 +95,56 @@ class ComposeResourceTest { fun testReadStringResource() = runComposeUiTest { runBlockingTest { setContent { - assertEquals("Compose Resources App", getString(TestStringResource("app_name"))) + assertEquals("Compose Resources App", stringResource(TestStringResource("app_name"))) assertEquals( "Hello, test-name! You have 42 new messages.", - getString(TestStringResource("str_template"), "test-name", 42) + stringResource(TestStringResource("str_template"), "test-name", 42) ) - assertEquals(listOf("item 1", "item 2", "item 3"), getStringArray(TestStringResource("str_arr"))) + assertEquals(listOf("item 1", "item 2", "item 3"), stringArrayResource(TestStringResource("str_arr"))) } awaitIdle() } } + + @Test + fun testLoadStringResource() = runBlockingTest { + kotlin.test.assertEquals("Compose Resources App", getString(TestStringResource("app_name"))) + kotlin.test.assertEquals( + "Hello, test-name! You have 42 new messages.", + getString(TestStringResource("str_template"), "test-name", 42) + ) + kotlin.test.assertEquals(listOf("item 1", "item 2", "item 3"), getStringArray(TestStringResource("str_arr"))) + } + + @Test + fun testMissingResource() = runBlockingTest { + assertFailsWith { + readResourceBytes("missing.png") + } + val error = assertFailsWith { + getString(TestStringResource("unknown_id")) + } + kotlin.test.assertEquals("String ID=`unknown_id` is not found!", error.message) + } + + @Test + fun testReadFileResource() = runBlockingTest { + val bytes = readResourceBytes("strings.xml") + kotlin.test.assertEquals( + """ + + Compose Resources App + 😊 Hello world! + Hello, %1${'$'}s! You have %2${'$'}d new messages. + + item 1 + item 2 + item 3 + + + + """.trimIndent(), + bytes.decodeToString() + ) + } } diff --git a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt index ab11f6a7db..ce46e8da00 100644 --- a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt +++ b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt @@ -1,14 +1,14 @@ package org.jetbrains.compose.resources import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.font.* @ExperimentalResourceApi @Composable actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): Font { - val path = resource.getPathByEnvironment() + val environment = rememberEnvironment() + val path = remember(environment) { resource.getPathByEnvironment(environment) } return Font(path, LocalContext.current.assets, weight, style) } \ No newline at end of file diff --git a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.android.kt b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.android.kt new file mode 100644 index 0000000000..e32e51290b --- /dev/null +++ b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.android.kt @@ -0,0 +1,18 @@ +package org.jetbrains.compose.resources + +import android.content.res.Configuration +import android.content.res.Resources +import java.util.* + +internal actual fun getResourceEnvironment(): ResourceEnvironment { + val locale = Locale.getDefault() + val configuration = Resources.getSystem().configuration + val isDarkTheme = configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + val dpi = configuration.densityDpi + return ResourceEnvironment( + language = LanguageQualifier(locale.language), + region = RegionQualifier(locale.country), + theme = ThemeQualifier.selectByValue(isDarkTheme), + density = DensityQualifier.selectByValue(dpi) + ) +} \ No newline at end of file diff --git a/components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/PlatformState.blocking.kt b/components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/PlatformState.blocking.kt deleted file mode 100644 index f54c6a3c93..0000000000 --- a/components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/PlatformState.blocking.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.jetbrains.compose.resources - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import kotlinx.coroutines.runBlocking - -@Composable -internal actual fun rememberState(key: Any, getDefault: () -> T, block: suspend () -> T): State = remember(key) { - mutableStateOf( - runBlocking { block() } - ) -} \ No newline at end of file diff --git a/components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/ResourceState.blocking.kt b/components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/ResourceState.blocking.kt new file mode 100644 index 0000000000..0d6d272dda --- /dev/null +++ b/components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/ResourceState.blocking.kt @@ -0,0 +1,18 @@ +package org.jetbrains.compose.resources + +import androidx.compose.runtime.* +import kotlinx.coroutines.runBlocking + +@Composable +internal actual fun rememberResourceState( + key: Any, + getDefault: () -> T, + block: suspend (ResourceEnvironment) -> T +): State { + val environment = rememberEnvironment() + return remember(key, environment) { + mutableStateOf( + runBlocking { block(environment) } + ) + } +} \ No newline at end of file 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 cc2e80cf52..657d6d4dae 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,9 +2,7 @@ package org.jetbrains.compose.resources import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.font.* /** * Represents a font resource. 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 5a5bd1bc1e..a589e6d920 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 @@ -1,9 +1,6 @@ package org.jetbrains.compose.resources -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.Painter @@ -11,46 +8,44 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.jetbrains.compose.resources.vector.toImageVector import org.jetbrains.compose.resources.vector.xmldom.Element /** - * Represents an image resource. + * Represents a drawable resource. * - * @param id The unique identifier of the image resource. + * @param id The unique identifier of the drawable resource. * @param items The set of resource items associated with the image resource. */ @Immutable -class ImageResource(id: String, items: Set) : Resource(id, items) +class DrawableResource(id: String, items: Set) : Resource(id, items) /** - * Creates an [ImageResource] object with the specified path. + * Creates an [DrawableResource] object with the specified path. * - * @param path The path of the image resource. - * @return An [ImageResource] object. + * @param path The path of the drawable resource. + * @return An [DrawableResource] object. */ -fun ImageResource(path: String): ImageResource = ImageResource( - id = "ImageResource:$path", +fun DrawableResource(path: String): DrawableResource = DrawableResource( + id = "DrawableResource:$path", items = setOf(ResourceItem(emptySet(), path)) ) /** - * Retrieves a [Painter] using the specified image resource. + * Retrieves a [Painter] using the specified drawable resource. * Automatically select a type of the Painter depending on the file extension. * - * @param resource The image resource to be used. + * @param resource The drawable resource to be used. * @return The [Painter] loaded from the resource. */ @ExperimentalResourceApi @Composable -fun painterResource(resource: ImageResource): Painter { - val filePath = remember(resource) { resource.getPathByEnvironment() } +fun painterResource(resource: DrawableResource): Painter { + val environment = rememberEnvironment() + val filePath = remember(resource, environment) { resource.getPathByEnvironment(environment) } val isXml = filePath.endsWith(".xml", true) if (isXml) { return rememberVectorPainter(vectorResource(resource)) @@ -62,17 +57,17 @@ fun painterResource(resource: ImageResource): Painter { private val emptyImageBitmap: ImageBitmap by lazy { ImageBitmap(1, 1) } /** - * Retrieves an ImageBitmap using the specified image resource. + * Retrieves an ImageBitmap using the specified drawable resource. * - * @param resource The image resource to be used. + * @param resource The drawable resource to be used. * @return The ImageBitmap loaded from the resource. */ @ExperimentalResourceApi @Composable -fun imageResource(resource: ImageResource): ImageBitmap { +fun imageResource(resource: DrawableResource): ImageBitmap { val resourceReader = LocalResourceReader.current - val imageBitmap by rememberState(resource, { emptyImageBitmap }) { - val path = resource.getPathByEnvironment() + val imageBitmap by rememberResourceState(resource, { emptyImageBitmap }) { env -> + val path = resource.getPathByEnvironment(env) val cached = loadImage(path, resourceReader) { ImageCache.Bitmap(it.toImageBitmap()) } as ImageCache.Bitmap @@ -86,18 +81,18 @@ private val emptyImageVector: ImageVector by lazy { } /** - * Retrieves an ImageVector using the specified image resource. + * Retrieves an ImageVector using the specified drawable resource. * - * @param resource The image resource to be used. + * @param resource The drawable resource to be used. * @return The ImageVector loaded from the resource. */ @ExperimentalResourceApi @Composable -fun vectorResource(resource: ImageResource): ImageVector { +fun vectorResource(resource: DrawableResource): ImageVector { val resourceReader = LocalResourceReader.current val density = LocalDensity.current - val imageVector by rememberState(resource, { emptyImageVector }) { - val path = resource.getPathByEnvironment() + val imageVector by rememberResourceState(resource, { emptyImageVector }) { env -> + val path = resource.getPathByEnvironment(env) val cached = loadImage(path, resourceReader) { ImageCache.Vector(it.toXmlElement().toImageVector(density)) } as ImageCache.Vector diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Qualifier.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Qualifier.kt new file mode 100644 index 0000000000..95a4e7829a --- /dev/null +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Qualifier.kt @@ -0,0 +1,50 @@ +package org.jetbrains.compose.resources + +interface Qualifier + +data class LanguageQualifier( + val language: String +) : Qualifier + +data class RegionQualifier( + val region: String +) : Qualifier + +enum class ThemeQualifier : Qualifier { + LIGHT, + DARK; + + companion object { + fun selectByValue(isDark: Boolean) = + if (isDark) DARK else LIGHT + } +} + +//https://developer.android.com/guide/topics/resources/providing-resources +enum class DensityQualifier(val dpi: Int) : Qualifier { + LDPI(120), + MDPI(160), + HDPI(240), + XHDPI(320), + XXHDPI(480), + XXXHDPI(640); + + companion object { + fun selectByValue(dpi: Int) = when { + dpi <= LDPI.dpi -> LDPI + dpi <= MDPI.dpi -> MDPI + dpi <= HDPI.dpi -> HDPI + dpi <= XHDPI.dpi -> XHDPI + dpi <= XXHDPI.dpi -> XXHDPI + else -> XXXHDPI + } + fun selectByDensity(density: Float) = when { + density <= 0.75 -> LDPI + density <= 1.0 -> MDPI + density <= 1.33 -> HDPI + density <= 2.0 -> XHDPI + density <= 3.0 -> XXHDPI + else -> XXXHDPI + } + } +} diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.kt index a4e100e58f..f9fe27a347 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.kt @@ -38,11 +38,6 @@ sealed class Resource( */ @Immutable data class ResourceItem( - internal val qualifiers: Set, + internal val qualifiers: Set, internal val path: String ) - -internal fun Resource.getPathByEnvironment(): String { - //TODO - return items.first().path -} \ No newline at end of file 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 new file mode 100644 index 0000000000..eee704ff25 --- /dev/null +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.kt @@ -0,0 +1,73 @@ +package org.jetbrains.compose.resources + +import androidx.compose.foundation.isSystemInDarkTheme +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 +) + +@Composable +internal fun rememberEnvironment(): ResourceEnvironment { + val composeLocale = Locale.current + val composeTheme = isSystemInDarkTheme() + val composeDensity = LocalDensity.current + + //cache ResourceEnvironment unless compose environment is changed + return remember(composeLocale, composeTheme, composeDensity) { + ResourceEnvironment( + LanguageQualifier(composeLocale.language), + RegionQualifier(composeLocale.region), + ThemeQualifier.selectByValue(composeTheme), + DensityQualifier.selectByDensity(composeDensity.density) + ) + } +} + +/** + * Provides the resource environment for non-composable access to string resources. + * It is an expensive operation! Don't use it in composable functions with no cache! + */ +internal expect fun getResourceEnvironment(): ResourceEnvironment + +internal fun Resource.getPathByEnvironment(environment: ResourceEnvironment): String { + //Priority of environments: https://developer.android.com/guide/topics/resources/providing-resources#table2 + items.toList() + .filterBy(environment.language) + .also { if (it.size == 1) return it.first().path } + .filterBy(environment.region) + .also { if (it.size == 1) return it.first().path } + .filterBy(environment.theme) + .also { if (it.size == 1) return it.first().path } + .filterBy(environment.density) + .also { if (it.size == 1) return it.first().path } + .let { items -> + if (items.isEmpty()) { + error("Resource with ID='$id' not found") + } else { + error("Resource with ID='$id' has more than one file: ${items.joinToString { it.path }}") + } + } +} + +private fun List.filterBy(qualifier: Qualifier): List { + //Android has a slightly different algorithm, + //but it provides the same result: https://developer.android.com/guide/topics/resources/providing-resources#BestMatch + + //filter items with the requested qualifier + val withQualifier = filter { item -> + item.qualifiers.any { it == qualifier } + } + + if (withQualifier.isNotEmpty()) return withQualifier + + //items with no requested qualifier type (default) + return filter { item -> + item.qualifiers.none { it::class == qualifier::class } + } +} \ No newline at end of file diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PlatformState.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceState.kt similarity index 80% rename from components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PlatformState.kt rename to components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceState.kt index eb84711edb..f09ba40827 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PlatformState.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceState.kt @@ -9,8 +9,8 @@ import androidx.compose.runtime.State * On the JS platform it loads the state asynchronously and uses `getDefault` as an initial state value. */ @Composable -internal expect fun rememberState( +internal expect fun rememberResourceState( key: Any, getDefault: () -> T, - block: suspend () -> T + block: suspend (ResourceEnvironment) -> T ): State \ No newline at end of file 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 32e797b4b5..527d337393 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 @@ -1,12 +1,7 @@ package org.jetbrains.compose.resources -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.getValue -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope +import androidx.compose.runtime.* +import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.jetbrains.compose.resources.vector.xmldom.Element @@ -76,9 +71,11 @@ private suspend fun parseStringXml(path: String, resourceReader: ResourceReader) */ @ExperimentalResourceApi @Composable -fun getString(resource: StringResource): String { +fun stringResource(resource: StringResource): String { val resourceReader = LocalResourceReader.current - val str by rememberState(resource, { "" }) { loadString(resource, resourceReader) } + val str by rememberResourceState(resource, { "" }) { env -> + loadString(resource, resourceReader, env) + } return str } @@ -91,10 +88,15 @@ fun getString(resource: StringResource): String { * @throws IllegalArgumentException If the provided ID is not found in the resource file. */ @ExperimentalResourceApi -suspend fun loadString(resource: StringResource): String = loadString(resource, DefaultResourceReader) - -private suspend fun loadString(resource: StringResource, resourceReader: ResourceReader): String { - val path = resource.getPathByEnvironment() +suspend fun getString(resource: StringResource): String = + loadString(resource, DefaultResourceReader, getResourceEnvironment()) + +private suspend fun loadString( + resource: StringResource, + resourceReader: ResourceReader, + environment: ResourceEnvironment +): String { + val path = resource.getPathByEnvironment(environment) val keyToValue = getParsedStrings(path, resourceReader) val item = keyToValue[resource.key] as? StringItem.Value ?: error("String ID=`${resource.key}` is not found!") @@ -112,10 +114,12 @@ private suspend fun loadString(resource: StringResource, resourceReader: Resourc */ @ExperimentalResourceApi @Composable -fun getString(resource: StringResource, vararg formatArgs: Any): String { +fun stringResource(resource: StringResource, vararg formatArgs: Any): String { val resourceReader = LocalResourceReader.current val args = formatArgs.map { it.toString() } - val str by rememberState(resource, { "" }) { loadString(resource, args, resourceReader) } + val str by rememberResourceState(resource, { "" }) { env -> + loadString(resource, args, resourceReader, env) + } return str } @@ -129,14 +133,20 @@ fun getString(resource: StringResource, vararg formatArgs: Any): String { * @throws IllegalArgumentException If the provided ID is not found in the resource file. */ @ExperimentalResourceApi -suspend fun loadString(resource: StringResource, vararg formatArgs: Any): String = loadString( +suspend fun getString(resource: StringResource, vararg formatArgs: Any): String = loadString( resource, formatArgs.map { it.toString() }, - DefaultResourceReader + DefaultResourceReader, + getResourceEnvironment() ) -private suspend fun loadString(resource: StringResource, args: List, resourceReader: ResourceReader): String { - val str = loadString(resource, resourceReader) +private suspend fun loadString( + resource: StringResource, + args: List, + resourceReader: ResourceReader, + environment: ResourceEnvironment +): String { + val str = loadString(resource, resourceReader, environment) return SimpleStringFormatRegex.replace(str) { matchResult -> args[matchResult.groupValues[1].toInt() - 1] } @@ -152,9 +162,11 @@ private suspend fun loadString(resource: StringResource, args: List, res */ @ExperimentalResourceApi @Composable -fun getStringArray(resource: StringResource): List { +fun stringArrayResource(resource: StringResource): List { val resourceReader = LocalResourceReader.current - val array by rememberState(resource, { emptyList() }) { loadStringArray(resource, resourceReader) } + val array by rememberResourceState(resource, { emptyList() }) { env -> + loadStringArray(resource, resourceReader, env) + } return array } @@ -167,10 +179,15 @@ fun getStringArray(resource: StringResource): List { * @throws IllegalStateException if the string array with the given ID is not found. */ @ExperimentalResourceApi -suspend fun loadStringArray(resource: StringResource): List = loadStringArray(resource, DefaultResourceReader) - -private suspend fun loadStringArray(resource: StringResource, resourceReader: ResourceReader): List { - val path = resource.getPathByEnvironment() +suspend fun getStringArray(resource: StringResource): List = + loadStringArray(resource, DefaultResourceReader, getResourceEnvironment()) + +private suspend fun loadStringArray( + resource: StringResource, + resourceReader: ResourceReader, + environment: ResourceEnvironment +): List { + val path = resource.getPathByEnvironment(environment) val keyToValue = getParsedStrings(path, resourceReader) val item = keyToValue[resource.key] as? StringItem.Array ?: error("String array ID=`${resource.key}` is not found!") 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 index cd90bd2478..5c5c828748 100644 --- 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 @@ -5,62 +5,98 @@ package org.jetbrains.compose.resources -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertNotEquals +import org.jetbrains.compose.resources.DensityQualifier.* +import org.jetbrains.compose.resources.ThemeQualifier.DARK +import org.jetbrains.compose.resources.ThemeQualifier.LIGHT +import kotlin.test.* -@OptIn(ExperimentalResourceApi::class) class ResourceTest { @Test fun testResourceEquals() = runBlockingTest { - assertEquals(ImageResource("a"), ImageResource("a")) + assertEquals(DrawableResource("a"), DrawableResource("a")) } @Test fun testResourceNotEquals() = runBlockingTest { - assertNotEquals(ImageResource("a"), ImageResource("b")) + assertNotEquals(DrawableResource("a"), DrawableResource("b")) } @Test - fun testMissingResource() = runBlockingTest { - assertFailsWith { - readResourceBytes("missing.png") - } - val error = assertFailsWith { - loadString(TestStringResource("unknown_id")) - } - assertEquals("String ID=`unknown_id` is not found!", error.message) - } - - @Test - fun testReadFileResource() = runBlockingTest { - val bytes = readResourceBytes("strings.xml") + fun testGetPathByEnvironment() { + val resource = DrawableResource( + id = "ImageResource:test", + items = setOf( + ResourceItem(setOf(), "default"), + ResourceItem(setOf(LanguageQualifier("en")), "en"), + ResourceItem(setOf(LanguageQualifier("en"), RegionQualifier("US"), XHDPI), "en-rUS-xhdpi"), + ResourceItem(setOf(LanguageQualifier("fr"), LIGHT), "fr-light"), + ResourceItem(setOf(DARK), "dark"), + ) + ) + fun env(lang: String, reg: String, theme: ThemeQualifier, density: DensityQualifier) = ResourceEnvironment( + language = LanguageQualifier(lang), + region = RegionQualifier(reg), + theme = theme, + density = density + ) assertEquals( - """ - - Compose Resources App - 😊 Hello world! - Hello, %1${'$'}s! You have %2${'$'}d new messages. - - item 1 - item 2 - item 3 - - - - """.trimIndent(), - bytes.decodeToString() + "en-rUS-xhdpi", + resource.getPathByEnvironment(env("en", "US", DARK, XXHDPI)) + ) + assertEquals( + "en", + resource.getPathByEnvironment(env("en", "IN", LIGHT, LDPI)) ) - } - - @Test - fun testLoadStringResource() = runBlockingTest { - assertEquals("Compose Resources App", loadString(TestStringResource("app_name"))) assertEquals( - "Hello, test-name! You have 42 new messages.", - loadString(TestStringResource("str_template"), "test-name", 42) + "default", + resource.getPathByEnvironment(env("ch", "", LIGHT, MDPI)) ) - assertEquals(listOf("item 1", "item 2", "item 3"), loadStringArray(TestStringResource("str_arr"))) + assertEquals( + "dark", + resource.getPathByEnvironment(env("ch", "", DARK, MDPI)) + ) + assertEquals( + "fr-light", + resource.getPathByEnvironment(env("fr", "", DARK, MDPI)) + ) + assertEquals( + "fr-light", + resource.getPathByEnvironment(env("fr", "IN", LIGHT, MDPI)) + ) + assertEquals( + "default", + resource.getPathByEnvironment(env("ru", "US", LIGHT, XHDPI)) + ) + assertEquals( + "dark", + resource.getPathByEnvironment(env("ru", "US", DARK, XHDPI)) + ) + + val resourceWithNoDefault = DrawableResource( + id = "ImageResource:test2", + items = setOf( + ResourceItem(setOf(LanguageQualifier("en")), "en"), + ResourceItem(setOf(LanguageQualifier("fr"), LIGHT), "fr-light") + ) + ) + assertFailsWith { + resourceWithNoDefault.getPathByEnvironment(env("ru", "US", DARK, XHDPI)) + }.message.let { msg -> + assertEquals("Resource with ID='ImageResource:test2' not found", msg) + } + + val resourceWithFewFiles = DrawableResource( + id = "ImageResource:test3", + items = setOf( + ResourceItem(setOf(LanguageQualifier("en")), "en1"), + ResourceItem(setOf(LanguageQualifier("en")), "en2") + ) + ) + assertFailsWith { + resourceWithFewFiles.getPathByEnvironment(env("en", "US", DARK, XHDPI)) + }.message.let { msg -> + assertEquals("Resource with ID='ImageResource:test3' has more than one file: en1, en2", msg) + } + } } diff --git a/components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.desktop.kt b/components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.desktop.kt new file mode 100644 index 0000000000..7e2eb848a4 --- /dev/null +++ b/components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.desktop.kt @@ -0,0 +1,19 @@ +package org.jetbrains.compose.resources + +import org.jetbrains.skiko.SystemTheme +import org.jetbrains.skiko.currentSystemTheme +import java.awt.Toolkit +import java.util.* + +internal actual fun getResourceEnvironment(): ResourceEnvironment { + val locale = Locale.getDefault() + //FIXME: don't use skiko internals + val isDarkTheme = currentSystemTheme == SystemTheme.DARK + val dpi = Toolkit.getDefaultToolkit().screenResolution + return ResourceEnvironment( + language = LanguageQualifier(locale.language), + region = RegionQualifier(locale.country), + theme = ThemeQualifier.selectByValue(isDarkTheme), + density = DensityQualifier.selectByValue(dpi) + ) +} \ No newline at end of file diff --git a/components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.desktop.kt b/components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.desktop.kt index aed5d003a4..a70993c189 100644 --- a/components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.desktop.kt +++ b/components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.desktop.kt @@ -2,9 +2,7 @@ package org.jetbrains.compose.resources import androidx.compose.foundation.Image import androidx.compose.material3.Text -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue +import androidx.compose.runtime.* import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.runComposeUiTest import kotlinx.coroutines.flow.MutableStateFlow @@ -12,6 +10,7 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith @OptIn(ExperimentalResourceApi::class, ExperimentalTestApi::class) class ComposeResourceTest { @@ -25,7 +24,7 @@ class ComposeResourceTest { @Test fun testCountRecompositions() = runComposeUiTest { runBlockingTest { - val imagePathFlow = MutableStateFlow(ImageResource("1.png")) + val imagePathFlow = MutableStateFlow(DrawableResource("1.png")) val recompositionsCounter = RecompositionsCounter() setContent { val res by imagePathFlow.collectAsState() @@ -35,7 +34,7 @@ class ComposeResourceTest { } } awaitIdle() - imagePathFlow.emit(ImageResource("2.png")) + imagePathFlow.emit(DrawableResource("2.png")) awaitIdle() assertEquals(2, recompositionsCounter.count) } @@ -45,7 +44,7 @@ class ComposeResourceTest { fun testImageResourceCache() = runComposeUiTest { runBlockingTest { val testResourceReader = TestResourceReader() - val imagePathFlow = MutableStateFlow(ImageResource("1.png")) + val imagePathFlow = MutableStateFlow(DrawableResource("1.png")) setContent { CompositionLocalProvider(LocalResourceReader provides testResourceReader) { val res by imagePathFlow.collectAsState() @@ -53,9 +52,9 @@ class ComposeResourceTest { } } awaitIdle() - imagePathFlow.emit(ImageResource("2.png")) + imagePathFlow.emit(DrawableResource("2.png")) awaitIdle() - imagePathFlow.emit(ImageResource("1.png")) + imagePathFlow.emit(DrawableResource("1.png")) awaitIdle() assertEquals( @@ -73,8 +72,8 @@ class ComposeResourceTest { setContent { CompositionLocalProvider(LocalResourceReader provides testResourceReader) { val res by stringIdFlow.collectAsState() - Text(getString(res)) - Text(getStringArray(TestStringResource("str_arr")).joinToString()) + Text(stringResource(res)) + Text(stringArrayResource(TestStringResource("str_arr")).joinToString()) } } awaitIdle() @@ -94,14 +93,56 @@ class ComposeResourceTest { fun testReadStringResource() = runComposeUiTest { runBlockingTest { setContent { - assertEquals("Compose Resources App", getString(TestStringResource("app_name"))) + assertEquals("Compose Resources App", stringResource(TestStringResource("app_name"))) assertEquals( "Hello, test-name! You have 42 new messages.", - getString(TestStringResource("str_template"), "test-name", 42) + stringResource(TestStringResource("str_template"), "test-name", 42) ) - assertEquals(listOf("item 1", "item 2", "item 3"), getStringArray(TestStringResource("str_arr"))) + assertEquals(listOf("item 1", "item 2", "item 3"), stringArrayResource(TestStringResource("str_arr"))) } awaitIdle() } } + + @Test + fun testLoadStringResource() = runBlockingTest { + kotlin.test.assertEquals("Compose Resources App", getString(TestStringResource("app_name"))) + kotlin.test.assertEquals( + "Hello, test-name! You have 42 new messages.", + getString(TestStringResource("str_template"), "test-name", 42) + ) + kotlin.test.assertEquals(listOf("item 1", "item 2", "item 3"), getStringArray(TestStringResource("str_arr"))) + } + + @Test + fun testMissingResource() = runBlockingTest { + assertFailsWith { + readResourceBytes("missing.png") + } + val error = assertFailsWith { + getString(TestStringResource("unknown_id")) + } + kotlin.test.assertEquals("String ID=`unknown_id` is not found!", error.message) + } + + @Test + fun testReadFileResource() = runBlockingTest { + val bytes = readResourceBytes("strings.xml") + kotlin.test.assertEquals( + """ + + Compose Resources App + 😊 Hello world! + Hello, %1${'$'}s! You have %2${'$'}d new messages. + + item 1 + item 2 + item 3 + + + + """.trimIndent(), + bytes.decodeToString() + ) + } } diff --git a/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.ios.kt b/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.ios.kt new file mode 100644 index 0000000000..da4f438363 --- /dev/null +++ b/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.ios.kt @@ -0,0 +1,21 @@ +package org.jetbrains.compose.resources + +import platform.Foundation.* +import platform.UIKit.UIScreen +import platform.UIKit.UIUserInterfaceStyle + +internal actual fun getResourceEnvironment(): ResourceEnvironment { + val locale = NSLocale.currentLocale() + + val mainScreen = UIScreen.mainScreen + val isDarkTheme = mainScreen.traitCollection().userInterfaceStyle == UIUserInterfaceStyle.UIUserInterfaceStyleDark + + //there is no an API to get a physical screen size and calculate a real DPI + val density = mainScreen.scale.toFloat() + return ResourceEnvironment( + language = LanguageQualifier(locale.languageCode), + region = RegionQualifier(locale.regionCode.orEmpty()), + theme = ThemeQualifier.selectByValue(isDarkTheme), + density = DensityQualifier.selectByDensity(density) + ) +} \ No newline at end of file diff --git a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ImageResources.js.kt b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ImageResources.js.kt index f9c23a0973..725ffd532d 100644 --- a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ImageResources.js.kt +++ b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ImageResources.js.kt @@ -1,8 +1,6 @@ package org.jetbrains.compose.resources -import org.jetbrains.compose.resources.vector.xmldom.Element -import org.jetbrains.compose.resources.vector.xmldom.ElementImpl -import org.jetbrains.compose.resources.vector.xmldom.MalformedXMLException +import org.jetbrains.compose.resources.vector.xmldom.* import org.w3c.dom.parsing.DOMParser internal actual fun ByteArray.toXmlElement(): Element { diff --git a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.js.kt b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.js.kt new file mode 100644 index 0000000000..d81bccce7b --- /dev/null +++ b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.js.kt @@ -0,0 +1,23 @@ +package org.jetbrains.compose.resources + +import kotlinx.browser.window + +private external class Intl { + class Locale(locale: String) { + val language: String + val region: String + } +} + +internal actual fun getResourceEnvironment(): ResourceEnvironment { + val locale = Intl.Locale(window.navigator.language) + val isDarkTheme = window.matchMedia("(prefers-color-scheme: dark)").matches + //96 - standard browser DPI https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio + val dpi: Int = (window.devicePixelRatio * 96).toInt() + return ResourceEnvironment( + language = LanguageQualifier(locale.language), + region = RegionQualifier(locale.region), + theme = ThemeQualifier.selectByValue(isDarkTheme), + density = DensityQualifier.selectByValue(dpi) + ) +} diff --git a/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.macos.kt b/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.macos.kt new file mode 100644 index 0000000000..91c8cf20c6 --- /dev/null +++ b/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.macos.kt @@ -0,0 +1,32 @@ +package org.jetbrains.compose.resources + +import kotlinx.cinterop.* +import platform.AppKit.NSScreen +import platform.CoreGraphics.CGDisplayPixelsWide +import platform.CoreGraphics.CGDisplayScreenSize +import platform.Foundation.* + +internal actual fun getResourceEnvironment(): ResourceEnvironment { + val locale = NSLocale.currentLocale() + val isDarkTheme = NSUserDefaults.standardUserDefaults.stringForKey("AppleInterfaceStyle") == "Dark" + + val dpi = NSScreen.mainScreen?.let { screen -> + val backingScaleFactor = screen.backingScaleFactor + val screenNumber = interpretObjCPointer( + screen.deviceDescription["NSScreenNumber"].objcPtr() + ).unsignedIntValue + + val displaySizePX = CGDisplayPixelsWide(screenNumber).toFloat() * backingScaleFactor + val displaySizeMM = CGDisplayScreenSize(screenNumber).useContents { width } + + //1 inch = 25.4 mm + ((displaySizePX / displaySizeMM) * 25.4f).toInt() + } ?: 0 + + return ResourceEnvironment( + language = LanguageQualifier(locale.languageCode), + region = RegionQualifier(locale.regionCode.orEmpty()), + theme = ThemeQualifier.selectByValue(isDarkTheme), + density = DensityQualifier.selectByValue(dpi) + ) +} \ No newline at end of file diff --git a/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt b/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt index 02ed4414ac..0bd899be07 100644 --- a/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt +++ b/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt @@ -2,9 +2,7 @@ package org.jetbrains.compose.resources import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.font.* import androidx.compose.ui.text.platform.Font import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi @@ -35,8 +33,8 @@ private val defaultEmptyFont by lazy { Font("org.jetbrains.compose.emptyFont", B @Composable actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): Font { val resourceReader = LocalResourceReader.current - val fontFile by rememberState(resource, { defaultEmptyFont }) { - val path = resource.getPathByEnvironment() + val fontFile by rememberResourceState(resource, { defaultEmptyFont }) { env -> + val path = resource.getPathByEnvironment(env) val fontBytes = resourceReader.read(path) Font(path, fontBytes, weight, style) } diff --git a/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.wasmJs.kt b/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.wasmJs.kt new file mode 100644 index 0000000000..4ab2d1cc05 --- /dev/null +++ b/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.wasmJs.kt @@ -0,0 +1,23 @@ +package org.jetbrains.compose.resources + +import kotlinx.browser.window + +private external class Intl { + class Locale(locale: String) { + val language: String + val region: String + } +} + +internal actual fun getResourceEnvironment(): ResourceEnvironment { + val locale = Intl.Locale(window.navigator.language) + val isDarkTheme = window.matchMedia("(prefers-color-scheme: dark)").matches + //96 - standard browser DPI https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio + val dpi: Int = (window.devicePixelRatio * 96).toInt() + return ResourceEnvironment( + language = LanguageQualifier(locale.language), + region = RegionQualifier(locale.region), + theme = ThemeQualifier.selectByValue(isDarkTheme), + density = DensityQualifier.selectByValue(dpi) + ) +} \ No newline at end of file diff --git a/components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/PlatformState.web.kt b/components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/ResourceState.web.kt similarity index 62% rename from components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/PlatformState.web.kt rename to components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/ResourceState.web.kt index 9ca7dd3ed9..705066c148 100644 --- a/components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/PlatformState.web.kt +++ b/components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/ResourceState.web.kt @@ -7,10 +7,15 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @Composable -internal actual fun rememberState(key: Any, getDefault: () -> T, block: suspend () -> T): State { +internal actual fun rememberResourceState( + key: Any, + getDefault: () -> T, + block: suspend (ResourceEnvironment) -> T +): State { + val environment = rememberEnvironment() val state = remember(key) { mutableStateOf(getDefault()) } LaunchedEffect(key) { - state.value = block() + state.value = block(environment) } return state } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidTargetConfiguration.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidTargetConfiguration.kt index a9aa1037fa..f5e5b4f981 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidTargetConfiguration.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidTargetConfiguration.kt @@ -25,7 +25,7 @@ internal fun Project.configureAndroidResources( val copyFonts = registerTask("copyFontsToAndroidAssets") { includeEmptyDirs = false from(commonResourcesDir) - include("**/fonts/*") + include("**/font*/*") into(androidFontsDir) onlyIf { onlyIfProvider.get() } } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt index 0ca3666b26..862c87d3e4 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt @@ -63,11 +63,11 @@ abstract class GenerateResClassTask : DefaultTask() { val file = this if (file.isDirectory) return null val dirName = file.parentFile.name ?: return null - val typeAndQualifiers = dirName.lowercase().split("-") + val typeAndQualifiers = dirName.split("-") if (typeAndQualifiers.isEmpty()) return null val typeString = typeAndQualifiers.first().lowercase() - val qualifiers = typeAndQualifiers.takeLast(typeAndQualifiers.size - 1).map { it.lowercase() }.toSet() + val qualifiers = typeAndQualifiers.takeLast(typeAndQualifiers.size - 1) val path = file.toPath().relativeTo(relativeTo) return if (typeString == "values" && file.name.equals("strings.xml", true)) { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt index a9063440e8..d168d05484 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt @@ -1,44 +1,102 @@ package org.jetbrains.compose.resources -import com.squareup.kotlinpoet.ClassName -import com.squareup.kotlinpoet.CodeBlock -import com.squareup.kotlinpoet.FileSpec -import com.squareup.kotlinpoet.KModifier -import com.squareup.kotlinpoet.PropertySpec -import com.squareup.kotlinpoet.TypeSpec -import com.squareup.kotlinpoet.withIndent +import com.squareup.kotlinpoet.* import java.nio.file.Path import kotlin.io.path.invariantSeparatorsPathString -import kotlin.io.path.pathString internal enum class ResourceType(val typeName: String) { - IMAGE("images"), - STRING("strings"), - FONT("fonts"); + DRAWABLE("drawable"), + STRING("string"), + FONT("font"); + + override fun toString(): String = typeName companion object { - fun fromString(str: String) = when (str) { - "images" -> ResourceType.IMAGE - "strings" -> ResourceType.STRING - "fonts" -> ResourceType.FONT - else -> error("Unknown resource type: $str") - } + fun fromString(str: String): ResourceType = + ResourceType.values() + .firstOrNull { it.typeName.equals(str, true) } + ?: error("Unknown resource type: '$str'.") } } internal data class ResourceItem( val type: ResourceType, - val qualifiers: Set, + val qualifiers: List, val name: String, val path: Path ) private fun ResourceItem.getClassName(): ClassName = when (type) { - ResourceType.IMAGE -> ClassName("org.jetbrains.compose.resources", "ImageResource") + ResourceType.DRAWABLE -> ClassName("org.jetbrains.compose.resources", "DrawableResource") ResourceType.STRING -> ClassName("org.jetbrains.compose.resources", "StringResource") ResourceType.FONT -> ClassName("org.jetbrains.compose.resources", "FontResource") } +private fun CodeBlock.Builder.addQualifiers(resourceItem: ResourceItem): CodeBlock.Builder { + val languageQualifier = ClassName("org.jetbrains.compose.resources", "LanguageQualifier") + val regionQualifier = ClassName("org.jetbrains.compose.resources", "RegionQualifier") + val themeQualifier = ClassName("org.jetbrains.compose.resources", "ThemeQualifier") + val densityQualifier = ClassName("org.jetbrains.compose.resources", "DensityQualifier") + + val languageRegex = Regex("[a-z][a-z]") + val regionRegex = Regex("r[A-Z][A-Z]") + + val qualifiersMap = mutableMapOf() + + fun saveQualifier(className: ClassName, qualifier: String) { + qualifiersMap[className]?.let { + error("${resourceItem.path} contains repetitive qualifiers: '$it' and '$qualifier'.") + } + qualifiersMap[className] = qualifier + } + + resourceItem.qualifiers.forEach { q -> + when (q) { + "light", + "dark" -> { + saveQualifier(themeQualifier, q) + } + + "mdpi", + "hdpi", + "xhdpi", + "xxhdpi", + "xxxhdpi", + "ldpi" -> { + saveQualifier(densityQualifier, q) + } + + else -> when { + q.matches(languageRegex) -> { + saveQualifier(languageQualifier, q) + } + + q.matches(regionRegex) -> { + saveQualifier(regionQualifier, q) + } + + else -> error("${resourceItem.path} contains unknown qualifier: '$q'.") + } + } + } + qualifiersMap[themeQualifier]?.let { q -> add("%T.${q.uppercase()}, ", themeQualifier) } + qualifiersMap[densityQualifier]?.let { q -> add("%T.${q.uppercase()}, ", densityQualifier) } + qualifiersMap[languageQualifier]?.let { q -> add("%T(\"$q\"), ", languageQualifier) } + qualifiersMap[regionQualifier]?.let { q -> + val lang = qualifiersMap[languageQualifier] + if (lang == null) { + error("Region qualifier must be used only with language.\nFile: ${resourceItem.path}") + } + val langAndRegion = "$lang-$q" + if(!resourceItem.path.toString().contains("-$langAndRegion")) { + error("Region qualifier must be declared after language: '$langAndRegion'.\nFile: ${resourceItem.path}") + } + add("%T(\"${q.takeLast(2)}\"), ", regionQualifier) + } + + return this +} + internal fun getResFileSpec( //type -> id -> items resources: Map>>, @@ -77,9 +135,12 @@ private fun TypeSpec.Builder.addResourceProperty(name: String, items: List - val qualifiers = item.qualifiers.sorted().joinToString { "\"$it\"" } - //file separator should be '/' on all platforms - add("%T(setOf($qualifiers), \"${item.path.invariantSeparatorsPathString}\"),\n", resourceItemClass) + add("%T(\n", resourceItemClass).withIndent { + add("setOf(").addQualifiers(item).add("),\n") + //file separator should be '/' on all platforms + add("\"${item.path.invariantSeparatorsPathString}\"\n") + } + add("),\n") } } add(")\n") diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt index 2f79dc57aa..951a7dbed9 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt @@ -17,12 +17,12 @@ class ResourcesTest : GradlePluginTestBase() { file("expected/Res.kt") ) check.logContains(""" - Unknown resource type: ignored + Unknown resource type: 'ignored'. """.trimIndent()) } - file("src/commonMain/resources/composeRes/images/vector_2.xml").renameTo( - file("src/commonMain/resources/composeRes/images/vector_3.xml") + file("src/commonMain/resources/composeRes/drawable/vector_2.xml").renameTo( + file("src/commonMain/resources/composeRes/drawable/vector_3.xml") ) //check resource's accessors were regenerated @@ -33,8 +33,52 @@ class ResourcesTest : GradlePluginTestBase() { ) } - file("src/commonMain/resources/composeRes/images/vector_3.xml").renameTo( - file("src/commonMain/resources/composeRes/images/vector_2.xml") + file("src/commonMain/resources/composeRes/drawable-en").renameTo( + file("src/commonMain/resources/composeRes/drawable-ren") + ) + + gradle("generateComposeResClass").checks { + check.logContains(""" + contains unknown qualifier: 'ren'. + """.trimIndent()) + } + + file("src/commonMain/resources/composeRes/drawable-ren").renameTo( + file("src/commonMain/resources/composeRes/drawable-rUS-en") + ) + + gradle("generateComposeResClass").checks { + check.logContains(""" + Region qualifier must be declared after language: 'en-rUS'. + """.trimIndent()) + } + + file("src/commonMain/resources/composeRes/drawable-rUS-en").renameTo( + file("src/commonMain/resources/composeRes/drawable-rUS") + ) + + gradle("generateComposeResClass").checks { + check.logContains(""" + Region qualifier must be used only with language. + """.trimIndent()) + } + + file("src/commonMain/resources/composeRes/drawable-rUS").renameTo( + file("src/commonMain/resources/composeRes/drawable-en-fr") + ) + + gradle("generateComposeResClass").checks { + check.logContains(""" + contains repetitive qualifiers: 'en' and 'fr'. + """.trimIndent()) + } + + file("src/commonMain/resources/composeRes/drawable-en-fr").renameTo( + file("src/commonMain/resources/composeRes/drawable-en") + ) + + file("src/commonMain/resources/composeRes/drawable/vector_3.xml").renameTo( + file("src/commonMain/resources/composeRes/drawable/vector_2.xml") ) //TODO: check a real build after a release a new version of the resources library diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt index 295839906e..4d8f56b575 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt +++ b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt @@ -1,84 +1,123 @@ package app.group.generated.resources +import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.FontResource -import org.jetbrains.compose.resources.ImageResource +import org.jetbrains.compose.resources.LanguageQualifier +import org.jetbrains.compose.resources.RegionQualifier import org.jetbrains.compose.resources.ResourceItem import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.ThemeQualifier internal object Res { - public object fonts { - public val emptyfont: FontResource = FontResource( - "FONT:emptyfont", + public object drawable { + public val _3_strange_name: DrawableResource = DrawableResource( + "drawable:_3_strange_name", setOf( - ResourceItem(setOf(), "composeRes/fonts/emptyFont.otf"), + ResourceItem( + setOf(), + "composeRes/drawable/3-strange-name.xml" + ), ) ) - } - public object images { - public val _3_strange_name: ImageResource = ImageResource( - "IMAGE:_3_strange_name", + public val vector: DrawableResource = DrawableResource( + "drawable:vector", setOf( - ResourceItem(setOf(), "composeRes/images/3-strange-name.xml"), + ResourceItem( + setOf(LanguageQualifier("au"), RegionQualifier("US"), ), + "composeRes/drawable-au-rUS/vector.xml" + ), + ResourceItem( + setOf(ThemeQualifier.DARK, LanguageQualifier("ge"), ), + "composeRes/drawable-dark-ge/vector.xml" + ), + ResourceItem( + setOf(LanguageQualifier("en"), ), + "composeRes/drawable-en/vector.xml" + ), + ResourceItem( + setOf(), + "composeRes/drawable/vector.xml" + ), ) ) - public val vector: ImageResource = ImageResource( - "IMAGE:vector", + public val vector_2: DrawableResource = DrawableResource( + "drawable:vector_2", setOf( - ResourceItem(setOf("q1", "q2"), "composeRes/images-q1-q2/vector.xml"), - ResourceItem(setOf("q1"), "composeRes/images-q1/vector.xml"), - ResourceItem(setOf("q2"), "composeRes/images-q2/vector.xml"), - ResourceItem(setOf(), "composeRes/images/vector.xml"), + ResourceItem( + setOf(), + "composeRes/drawable/vector_2.xml" + ), ) ) + } - public val vector_2: ImageResource = ImageResource( - "IMAGE:vector_2", + public object font { + public val emptyfont: FontResource = FontResource( + "font:emptyfont", setOf( - ResourceItem(setOf(), "composeRes/images/vector_2.xml"), + ResourceItem( + setOf(), + "composeRes/font/emptyFont.otf" + ), ) ) } - public object strings { + public object string { public val app_name: StringResource = StringResource( - "STRING:app_name", + "string:app_name", "app_name", setOf( - ResourceItem(setOf(), "composeRes/values/strings.xml"), + ResourceItem( + setOf(), + "composeRes/values/strings.xml" + ), ) ) public val hello: StringResource = StringResource( - "STRING:hello", + "string:hello", "hello", setOf( - ResourceItem(setOf(), "composeRes/values/strings.xml"), + ResourceItem( + setOf(), + "composeRes/values/strings.xml" + ), ) ) public val multi_line: StringResource = StringResource( - "STRING:multi_line", + "string:multi_line", "multi_line", setOf( - ResourceItem(setOf(), "composeRes/values/strings.xml"), + ResourceItem( + setOf(), + "composeRes/values/strings.xml" + ), ) ) public val str_arr: StringResource = StringResource( - "STRING:str_arr", + "string:str_arr", "str_arr", setOf( - ResourceItem(setOf(), "composeRes/values/strings.xml"), + ResourceItem( + setOf(), + "composeRes/values/strings.xml" + ), ) ) public val str_template: StringResource = StringResource( - "STRING:str_template", + "string:str_template", "str_template", setOf( - ResourceItem(setOf(), "composeRes/values/strings.xml"), + ResourceItem( + setOf(), + "composeRes/values/strings.xml" + ), ) ) } diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q1-q2/vector.xml b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable-au-rUS/vector.xml similarity index 100% rename from gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q1-q2/vector.xml rename to gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable-au-rUS/vector.xml diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q1/vector.xml b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable-dark-ge/vector.xml similarity index 100% rename from gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q1/vector.xml rename to gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable-dark-ge/vector.xml diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q2/vector.xml b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable-en/vector.xml similarity index 100% rename from gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q2/vector.xml rename to gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable-en/vector.xml diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images/3-strange-name.xml b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable/3-strange-name.xml similarity index 100% rename from gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images/3-strange-name.xml rename to gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable/3-strange-name.xml diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images/vector.xml b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable/vector.xml similarity index 100% rename from gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images/vector.xml rename to gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable/vector.xml diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images/vector_2.xml b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable/vector_2.xml similarity index 100% rename from gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images/vector_2.xml rename to gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable/vector_2.xml diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/fonts/emptyFont.otf b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/font/emptyFont.otf similarity index 100% rename from gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/fonts/emptyFont.otf rename to gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/font/emptyFont.otf