Browse Source

Get environment and select resource by qualifiers (#4018)

pull/4056/head
Konstantin 5 months ago committed by GitHub
parent
commit
27915cbc0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt
  2. 2
      components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FontRes.kt
  3. 32
      components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/ImagesRes.kt
  4. 24
      components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt
  5. 0
      components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/compose.png
  6. 0
      components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/droid_icon.xml
  7. 0
      components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/insta_icon.xml
  8. 0
      components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/land.webp
  9. 0
      components/resources/demo/shared/src/commonMain/resources/composeRes/font/font_awesome.otf
  10. 63
      components/resources/library/src/androidInstrumentedTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.android.kt
  11. 8
      components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt
  12. 18
      components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.android.kt
  13. 14
      components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/PlatformState.blocking.kt
  14. 18
      components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/ResourceState.blocking.kt
  15. 4
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt
  16. 55
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt
  17. 50
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Qualifier.kt
  18. 7
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.kt
  19. 73
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.kt
  20. 4
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceState.kt
  21. 67
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt
  22. 120
      components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ResourceTest.kt
  23. 19
      components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.desktop.kt
  24. 67
      components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.desktop.kt
  25. 21
      components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.ios.kt
  26. 4
      components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ImageResources.js.kt
  27. 23
      components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.js.kt
  28. 32
      components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.macos.kt
  29. 8
      components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt
  30. 23
      components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.wasmJs.kt
  31. 9
      components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/ResourceState.web.kt
  32. 2
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidTargetConfiguration.kt
  33. 4
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt
  34. 105
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt
  35. 54
      gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt
  36. 99
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt
  37. 0
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable-au-rUS/vector.xml
  38. 0
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable-dark-ge/vector.xml
  39. 0
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable-en/vector.xml
  40. 0
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable/3-strange-name.xml
  41. 0
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable/vector.xml
  42. 0
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable/vector_2.xml
  43. 0
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/font/emptyFont.otf

6
components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt

@ -28,7 +28,7 @@ fun FileRes(paddingValues: PaddingValues) {
) { ) {
Text( Text(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
text = "File: 'composeRes/images/droid_icon.xml'", text = "File: 'composeRes/drawable/droid_icon.xml'",
style = MaterialTheme.typography.titleLarge style = MaterialTheme.typography.titleLarge
) )
OutlinedCard( OutlinedCard(
@ -38,7 +38,7 @@ fun FileRes(paddingValues: PaddingValues) {
) { ) {
var bytes by remember { mutableStateOf(ByteArray(0)) } var bytes by remember { mutableStateOf(ByteArray(0)) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
bytes = readResourceBytes("composeRes/images/droid_icon.xml") bytes = readResourceBytes("composeRes/drawable/droid_icon.xml")
} }
Text( Text(
modifier = Modifier.padding(8.dp).height(200.dp).verticalScroll(rememberScrollState()), modifier = Modifier.padding(8.dp).height(200.dp).verticalScroll(rememberScrollState()),
@ -54,7 +54,7 @@ fun FileRes(paddingValues: PaddingValues) {
mutableStateOf(ByteArray(0)) mutableStateOf(ByteArray(0))
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
bytes = readBytes("composeRes/images/droid_icon.xml") bytes = readResourceBytes("composeRes/drawable/droid_icon.xml")
} }
Text(bytes.decodeToString()) Text(bytes.decodeToString())
""".trimIndent() """.trimIndent()

2
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) val symbols = arrayOf(0xf1ba, 0xf238, 0xf21a, 0xf1bb, 0xf1b8, 0xf09b, 0xf269, 0xf1d0, 0xf15a, 0xf293, 0xf1c6)
Text( Text(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),

32
components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/ImagesRes.kt

@ -28,13 +28,13 @@ fun ImagesRes(contentPadding: PaddingValues) {
) { ) {
Image( Image(
modifier = Modifier.size(100.dp), modifier = Modifier.size(100.dp),
painter = painterResource(Res.images.compose), painter = painterResource(Res.drawable.compose),
contentDescription = null contentDescription = null
) )
Text( Text(
""" """
Image( Image(
painter = painterResource(Res.images.compose) painter = painterResource(Res.drawable.compose)
) )
""".trimIndent() """.trimIndent()
) )
@ -47,13 +47,13 @@ fun ImagesRes(contentPadding: PaddingValues) {
) { ) {
Image( Image(
modifier = Modifier.size(100.dp), modifier = Modifier.size(100.dp),
painter = painterResource(Res.images.insta_icon), painter = painterResource(Res.drawable.insta_icon),
contentDescription = null contentDescription = null
) )
Text( Text(
""" """
Image( Image(
painter = painterResource(Res.images.insta_icon) painter = painterResource(Res.drawable.insta_icon)
) )
""".trimIndent() """.trimIndent()
) )
@ -66,13 +66,13 @@ fun ImagesRes(contentPadding: PaddingValues) {
) { ) {
Image( Image(
modifier = Modifier.size(140.dp), modifier = Modifier.size(140.dp),
bitmap = imageResource(Res.images.land), bitmap = imageResource(Res.drawable.land),
contentDescription = null contentDescription = null
) )
Text( Text(
""" """
Image( Image(
bitmap = imageResource(Res.images.land) bitmap = imageResource(Res.drawable.land)
) )
""".trimIndent() """.trimIndent()
) )
@ -85,13 +85,13 @@ fun ImagesRes(contentPadding: PaddingValues) {
) { ) {
Image( Image(
modifier = Modifier.size(100.dp), modifier = Modifier.size(100.dp),
imageVector = vectorResource(Res.images.droid_icon), imageVector = vectorResource(Res.drawable.droid_icon),
contentDescription = null contentDescription = null
) )
Text( Text(
""" """
Image( Image(
imageVector = vectorResource(Res.images.droid_icon) imageVector = vectorResource(Res.drawable.droid_icon)
) )
""".trimIndent() """.trimIndent()
) )
@ -104,13 +104,13 @@ fun ImagesRes(contentPadding: PaddingValues) {
) { ) {
Icon( Icon(
modifier = Modifier.size(100.dp), modifier = Modifier.size(100.dp),
painter = painterResource(Res.images.compose), painter = painterResource(Res.drawable.compose),
contentDescription = null contentDescription = null
) )
Text( Text(
""" """
Icon( Icon(
painter = painterResource(Res.images.compose) painter = painterResource(Res.drawable.compose)
) )
""".trimIndent() """.trimIndent()
) )
@ -123,13 +123,13 @@ fun ImagesRes(contentPadding: PaddingValues) {
) { ) {
Icon( Icon(
modifier = Modifier.size(100.dp), modifier = Modifier.size(100.dp),
painter = painterResource(Res.images.insta_icon), painter = painterResource(Res.drawable.insta_icon),
contentDescription = null contentDescription = null
) )
Text( Text(
""" """
Icon( Icon(
painter = painterResource(Res.images.insta_icon) painter = painterResource(Res.drawable.insta_icon)
) )
""".trimIndent() """.trimIndent()
) )
@ -142,13 +142,13 @@ fun ImagesRes(contentPadding: PaddingValues) {
) { ) {
Icon( Icon(
modifier = Modifier.size(140.dp), modifier = Modifier.size(140.dp),
bitmap = imageResource(Res.images.land), bitmap = imageResource(Res.drawable.land),
contentDescription = null contentDescription = null
) )
Text( Text(
""" """
Icon( Icon(
bitmap = imageResource(Res.images.land) bitmap = imageResource(Res.drawable.land)
) )
""".trimIndent() """.trimIndent()
) )
@ -161,13 +161,13 @@ fun ImagesRes(contentPadding: PaddingValues) {
) { ) {
Icon( Icon(
modifier = Modifier.size(100.dp), modifier = Modifier.size(100.dp),
imageVector = vectorResource(Res.images.droid_icon), imageVector = vectorResource(Res.drawable.droid_icon),
contentDescription = null contentDescription = null
) )
Text( Text(
""" """
Icon( Icon(
imageVector = vectorResource(Res.images.droid_icon) imageVector = vectorResource(Res.drawable.droid_icon)
) )
""".trimIndent() """.trimIndent()
) )

24
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.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import components.resources.demo.generated.resources.Res import components.resources.demo.generated.resources.Res
import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.resources.getStringArray import org.jetbrains.compose.resources.stringArrayResource
import org.jetbrains.compose.resources.readResourceBytes import org.jetbrains.compose.resources.readResourceBytes
@Composable @Composable
@ -54,9 +54,9 @@ fun StringRes(paddingValues: PaddingValues) {
} }
OutlinedTextField( OutlinedTextField(
modifier = Modifier.padding(16.dp).fillMaxWidth(), modifier = Modifier.padding(16.dp).fillMaxWidth(),
value = getString(Res.strings.app_name), value = stringResource(Res.string.app_name),
onValueChange = {}, onValueChange = {},
label = { Text("Text(getString(Res.strings.app_name)") }, label = { Text("Text(stringResource(Res.string.app_name)") },
enabled = false, enabled = false,
colors = TextFieldDefaults.colors( colors = TextFieldDefaults.colors(
disabledTextColor = MaterialTheme.colorScheme.onSurface, disabledTextColor = MaterialTheme.colorScheme.onSurface,
@ -66,9 +66,9 @@ fun StringRes(paddingValues: PaddingValues) {
) )
OutlinedTextField( OutlinedTextField(
modifier = Modifier.padding(16.dp).fillMaxWidth(), modifier = Modifier.padding(16.dp).fillMaxWidth(),
value = getString(Res.strings.hello), value = stringResource(Res.string.hello),
onValueChange = {}, onValueChange = {},
label = { Text("Text(getString(Res.strings.hello)") }, label = { Text("Text(stringResource(Res.string.hello)") },
enabled = false, enabled = false,
colors = TextFieldDefaults.colors( colors = TextFieldDefaults.colors(
disabledTextColor = MaterialTheme.colorScheme.onSurface, disabledTextColor = MaterialTheme.colorScheme.onSurface,
@ -78,9 +78,9 @@ fun StringRes(paddingValues: PaddingValues) {
) )
OutlinedTextField( OutlinedTextField(
modifier = Modifier.padding(16.dp).fillMaxWidth(), modifier = Modifier.padding(16.dp).fillMaxWidth(),
value = getString(Res.strings.multi_line), value = stringResource(Res.string.multi_line),
onValueChange = {}, onValueChange = {},
label = { Text("Text(getString(Res.strings.multi_line)") }, label = { Text("Text(stringResource(Res.string.multi_line)") },
enabled = false, enabled = false,
colors = TextFieldDefaults.colors( colors = TextFieldDefaults.colors(
disabledTextColor = MaterialTheme.colorScheme.onSurface, disabledTextColor = MaterialTheme.colorScheme.onSurface,
@ -90,9 +90,9 @@ fun StringRes(paddingValues: PaddingValues) {
) )
OutlinedTextField( OutlinedTextField(
modifier = Modifier.padding(16.dp).fillMaxWidth(), 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 = {}, 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, enabled = false,
colors = TextFieldDefaults.colors( colors = TextFieldDefaults.colors(
disabledTextColor = MaterialTheme.colorScheme.onSurface, disabledTextColor = MaterialTheme.colorScheme.onSurface,
@ -102,9 +102,9 @@ fun StringRes(paddingValues: PaddingValues) {
) )
OutlinedTextField( OutlinedTextField(
modifier = Modifier.padding(16.dp).fillMaxWidth(), modifier = Modifier.padding(16.dp).fillMaxWidth(),
value = getStringArray(Res.strings.str_arr).toString(), value = stringArrayResource(Res.string.str_arr).toString(),
onValueChange = {}, onValueChange = {},
label = { Text("Text(getStringArray(Res.strings.str_arr).toString())") }, label = { Text("Text(stringArrayResource(Res.string.str_arr).toString())") },
enabled = false, enabled = false,
colors = TextFieldDefaults.colors( colors = TextFieldDefaults.colors(
disabledTextColor = MaterialTheme.colorScheme.onSurface, disabledTextColor = MaterialTheme.colorScheme.onSurface,

0
components/resources/demo/shared/src/commonMain/resources/composeRes/images/compose.png → components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/compose.png

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

0
components/resources/demo/shared/src/commonMain/resources/composeRes/images/droid_icon.xml → components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/droid_icon.xml

0
components/resources/demo/shared/src/commonMain/resources/composeRes/images/insta_icon.xml → components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/insta_icon.xml

0
components/resources/demo/shared/src/commonMain/resources/composeRes/images/land.webp → components/resources/demo/shared/src/commonMain/resources/composeRes/drawable/land.webp

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

0
components/resources/demo/shared/src/commonMain/resources/composeRes/fonts/font_awesome.otf → components/resources/demo/shared/src/commonMain/resources/composeRes/font/font_awesome.otf

63
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.Before
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
@OptIn(ExperimentalResourceApi::class, ExperimentalTestApi::class) @OptIn(ExperimentalResourceApi::class, ExperimentalTestApi::class)
class ComposeResourceTest { class ComposeResourceTest {
@ -25,7 +26,7 @@ class ComposeResourceTest {
@Test @Test
fun testCountRecompositions() = runComposeUiTest { fun testCountRecompositions() = runComposeUiTest {
runBlockingTest { runBlockingTest {
val imagePathFlow = MutableStateFlow(ImageResource("1.png")) val imagePathFlow = MutableStateFlow(DrawableResource("1.png"))
val recompositionsCounter = RecompositionsCounter() val recompositionsCounter = RecompositionsCounter()
setContent { setContent {
val res by imagePathFlow.collectAsState() val res by imagePathFlow.collectAsState()
@ -35,7 +36,7 @@ class ComposeResourceTest {
} }
} }
awaitIdle() awaitIdle()
imagePathFlow.emit(ImageResource("2.png")) imagePathFlow.emit(DrawableResource("2.png"))
awaitIdle() awaitIdle()
assertEquals(2, recompositionsCounter.count) assertEquals(2, recompositionsCounter.count)
} }
@ -45,7 +46,7 @@ class ComposeResourceTest {
fun testImageResourceCache() = runComposeUiTest { fun testImageResourceCache() = runComposeUiTest {
runBlockingTest { runBlockingTest {
val testResourceReader = TestResourceReader() val testResourceReader = TestResourceReader()
val imagePathFlow = MutableStateFlow(ImageResource("1.png")) val imagePathFlow = MutableStateFlow(DrawableResource("1.png"))
setContent { setContent {
CompositionLocalProvider(LocalResourceReader provides testResourceReader) { CompositionLocalProvider(LocalResourceReader provides testResourceReader) {
val res by imagePathFlow.collectAsState() val res by imagePathFlow.collectAsState()
@ -53,9 +54,9 @@ class ComposeResourceTest {
} }
} }
awaitIdle() awaitIdle()
imagePathFlow.emit(ImageResource("2.png")) imagePathFlow.emit(DrawableResource("2.png"))
awaitIdle() awaitIdle()
imagePathFlow.emit(ImageResource("1.png")) imagePathFlow.emit(DrawableResource("1.png"))
awaitIdle() awaitIdle()
assertEquals( assertEquals(
@ -73,8 +74,8 @@ class ComposeResourceTest {
setContent { setContent {
CompositionLocalProvider(LocalResourceReader provides testResourceReader) { CompositionLocalProvider(LocalResourceReader provides testResourceReader) {
val res by stringIdFlow.collectAsState() val res by stringIdFlow.collectAsState()
Text(getString(res)) Text(stringResource(res))
Text(getStringArray(TestStringResource("str_arr")).joinToString()) Text(stringArrayResource(TestStringResource("str_arr")).joinToString())
} }
} }
awaitIdle() awaitIdle()
@ -94,14 +95,56 @@ class ComposeResourceTest {
fun testReadStringResource() = runComposeUiTest { fun testReadStringResource() = runComposeUiTest {
runBlockingTest { runBlockingTest {
setContent { setContent {
assertEquals("Compose Resources App", getString(TestStringResource("app_name"))) assertEquals("Compose Resources App", stringResource(TestStringResource("app_name")))
assertEquals( assertEquals(
"Hello, test-name! You have 42 new messages.", "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() 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<MissingResourceException> {
readResourceBytes("missing.png")
}
val error = assertFailsWith<IllegalStateException> {
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(
"""
<resources>
<string name="app_name">Compose Resources App</string>
<string name="hello">😊 Hello world!</string>
<string name="str_template">Hello, %1${'$'}s! You have %2${'$'}d new messages.</string>
<string-array name="str_arr">
<item>item 1</item>
<item>item 2</item>
<item>item 3</item>
</string-array>
</resources>
""".trimIndent(),
bytes.decodeToString()
)
}
} }

8
components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt

@ -1,14 +1,14 @@
package org.jetbrains.compose.resources package org.jetbrains.compose.resources
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
@ExperimentalResourceApi @ExperimentalResourceApi
@Composable @Composable
actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): Font { 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) return Font(path, LocalContext.current.assets, weight, style)
} }

18
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)
)
}

14
components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/PlatformState.blocking.kt

@ -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 <T> rememberState(key: Any, getDefault: () -> T, block: suspend () -> T): State<T> = remember(key) {
mutableStateOf(
runBlocking { block() }
)
}

18
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 <T> rememberResourceState(
key: Any,
getDefault: () -> T,
block: suspend (ResourceEnvironment) -> T
): State<T> {
val environment = rememberEnvironment()
return remember(key, environment) {
mutableStateOf(
runBlocking { block(environment) }
)
}
}

4
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.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
/** /**
* Represents a font resource. * Represents a font resource.

55
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt

@ -1,9 +1,6 @@
package org.jetbrains.compose.resources package org.jetbrains.compose.resources
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter 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.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.*
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import org.jetbrains.compose.resources.vector.toImageVector import org.jetbrains.compose.resources.vector.toImageVector
import org.jetbrains.compose.resources.vector.xmldom.Element 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. * @param items The set of resource items associated with the image resource.
*/ */
@Immutable @Immutable
class ImageResource(id: String, items: Set<ResourceItem>) : Resource(id, items) class DrawableResource(id: String, items: Set<ResourceItem>) : 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. * @param path The path of the drawable resource.
* @return An [ImageResource] object. * @return An [DrawableResource] object.
*/ */
fun ImageResource(path: String): ImageResource = ImageResource( fun DrawableResource(path: String): DrawableResource = DrawableResource(
id = "ImageResource:$path", id = "DrawableResource:$path",
items = setOf(ResourceItem(emptySet(), 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. * 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. * @return The [Painter] loaded from the resource.
*/ */
@ExperimentalResourceApi @ExperimentalResourceApi
@Composable @Composable
fun painterResource(resource: ImageResource): Painter { fun painterResource(resource: DrawableResource): Painter {
val filePath = remember(resource) { resource.getPathByEnvironment() } val environment = rememberEnvironment()
val filePath = remember(resource, environment) { resource.getPathByEnvironment(environment) }
val isXml = filePath.endsWith(".xml", true) val isXml = filePath.endsWith(".xml", true)
if (isXml) { if (isXml) {
return rememberVectorPainter(vectorResource(resource)) return rememberVectorPainter(vectorResource(resource))
@ -62,17 +57,17 @@ fun painterResource(resource: ImageResource): Painter {
private val emptyImageBitmap: ImageBitmap by lazy { ImageBitmap(1, 1) } 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. * @return The ImageBitmap loaded from the resource.
*/ */
@ExperimentalResourceApi @ExperimentalResourceApi
@Composable @Composable
fun imageResource(resource: ImageResource): ImageBitmap { fun imageResource(resource: DrawableResource): ImageBitmap {
val resourceReader = LocalResourceReader.current val resourceReader = LocalResourceReader.current
val imageBitmap by rememberState(resource, { emptyImageBitmap }) { val imageBitmap by rememberResourceState(resource, { emptyImageBitmap }) { env ->
val path = resource.getPathByEnvironment() val path = resource.getPathByEnvironment(env)
val cached = loadImage(path, resourceReader) { val cached = loadImage(path, resourceReader) {
ImageCache.Bitmap(it.toImageBitmap()) ImageCache.Bitmap(it.toImageBitmap())
} as ImageCache.Bitmap } 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. * @return The ImageVector loaded from the resource.
*/ */
@ExperimentalResourceApi @ExperimentalResourceApi
@Composable @Composable
fun vectorResource(resource: ImageResource): ImageVector { fun vectorResource(resource: DrawableResource): ImageVector {
val resourceReader = LocalResourceReader.current val resourceReader = LocalResourceReader.current
val density = LocalDensity.current val density = LocalDensity.current
val imageVector by rememberState(resource, { emptyImageVector }) { val imageVector by rememberResourceState(resource, { emptyImageVector }) { env ->
val path = resource.getPathByEnvironment() val path = resource.getPathByEnvironment(env)
val cached = loadImage(path, resourceReader) { val cached = loadImage(path, resourceReader) {
ImageCache.Vector(it.toXmlElement().toImageVector(density)) ImageCache.Vector(it.toXmlElement().toImageVector(density))
} as ImageCache.Vector } as ImageCache.Vector

50
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
}
}
}

7
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.kt

@ -38,11 +38,6 @@ sealed class Resource(
*/ */
@Immutable @Immutable
data class ResourceItem( data class ResourceItem(
internal val qualifiers: Set<String>, internal val qualifiers: Set<Qualifier>,
internal val path: String internal val path: String
) )
internal fun Resource.getPathByEnvironment(): String {
//TODO
return items.first().path
}

73
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<ResourceItem>.filterBy(qualifier: Qualifier): List<ResourceItem> {
//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 }
}
}

4
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PlatformState.kt → 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. * On the JS platform it loads the state asynchronously and uses `getDefault` as an initial state value.
*/ */
@Composable @Composable
internal expect fun <T> rememberState( internal expect fun <T> rememberResourceState(
key: Any, key: Any,
getDefault: () -> T, getDefault: () -> T,
block: suspend () -> T block: suspend (ResourceEnvironment) -> T
): State<T> ): State<T>

67
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt

@ -1,12 +1,7 @@
package org.jetbrains.compose.resources package org.jetbrains.compose.resources
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.Immutable import kotlinx.coroutines.*
import androidx.compose.runtime.getValue
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import org.jetbrains.compose.resources.vector.xmldom.Element import org.jetbrains.compose.resources.vector.xmldom.Element
@ -76,9 +71,11 @@ private suspend fun parseStringXml(path: String, resourceReader: ResourceReader)
*/ */
@ExperimentalResourceApi @ExperimentalResourceApi
@Composable @Composable
fun getString(resource: StringResource): String { fun stringResource(resource: StringResource): String {
val resourceReader = LocalResourceReader.current val resourceReader = LocalResourceReader.current
val str by rememberState(resource, { "" }) { loadString(resource, resourceReader) } val str by rememberResourceState(resource, { "" }) { env ->
loadString(resource, resourceReader, env)
}
return str return str
} }
@ -91,10 +88,15 @@ fun getString(resource: StringResource): String {
* @throws IllegalArgumentException If the provided ID is not found in the resource file. * @throws IllegalArgumentException If the provided ID is not found in the resource file.
*/ */
@ExperimentalResourceApi @ExperimentalResourceApi
suspend fun loadString(resource: StringResource): String = loadString(resource, DefaultResourceReader) suspend fun getString(resource: StringResource): String =
loadString(resource, DefaultResourceReader, getResourceEnvironment())
private suspend fun loadString(resource: StringResource, resourceReader: ResourceReader): String {
val path = resource.getPathByEnvironment() private suspend fun loadString(
resource: StringResource,
resourceReader: ResourceReader,
environment: ResourceEnvironment
): String {
val path = resource.getPathByEnvironment(environment)
val keyToValue = getParsedStrings(path, resourceReader) val keyToValue = getParsedStrings(path, resourceReader)
val item = keyToValue[resource.key] as? StringItem.Value val item = keyToValue[resource.key] as? StringItem.Value
?: error("String ID=`${resource.key}` is not found!") ?: error("String ID=`${resource.key}` is not found!")
@ -112,10 +114,12 @@ private suspend fun loadString(resource: StringResource, resourceReader: Resourc
*/ */
@ExperimentalResourceApi @ExperimentalResourceApi
@Composable @Composable
fun getString(resource: StringResource, vararg formatArgs: Any): String { fun stringResource(resource: StringResource, vararg formatArgs: Any): String {
val resourceReader = LocalResourceReader.current val resourceReader = LocalResourceReader.current
val args = formatArgs.map { it.toString() } 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 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. * @throws IllegalArgumentException If the provided ID is not found in the resource file.
*/ */
@ExperimentalResourceApi @ExperimentalResourceApi
suspend fun loadString(resource: StringResource, vararg formatArgs: Any): String = loadString( suspend fun getString(resource: StringResource, vararg formatArgs: Any): String = loadString(
resource, resource,
formatArgs.map { it.toString() }, formatArgs.map { it.toString() },
DefaultResourceReader DefaultResourceReader,
getResourceEnvironment()
) )
private suspend fun loadString(resource: StringResource, args: List<String>, resourceReader: ResourceReader): String { private suspend fun loadString(
val str = loadString(resource, resourceReader) resource: StringResource,
args: List<String>,
resourceReader: ResourceReader,
environment: ResourceEnvironment
): String {
val str = loadString(resource, resourceReader, environment)
return SimpleStringFormatRegex.replace(str) { matchResult -> return SimpleStringFormatRegex.replace(str) { matchResult ->
args[matchResult.groupValues[1].toInt() - 1] args[matchResult.groupValues[1].toInt() - 1]
} }
@ -152,9 +162,11 @@ private suspend fun loadString(resource: StringResource, args: List<String>, res
*/ */
@ExperimentalResourceApi @ExperimentalResourceApi
@Composable @Composable
fun getStringArray(resource: StringResource): List<String> { fun stringArrayResource(resource: StringResource): List<String> {
val resourceReader = LocalResourceReader.current 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 return array
} }
@ -167,10 +179,15 @@ fun getStringArray(resource: StringResource): List<String> {
* @throws IllegalStateException if the string array with the given ID is not found. * @throws IllegalStateException if the string array with the given ID is not found.
*/ */
@ExperimentalResourceApi @ExperimentalResourceApi
suspend fun loadStringArray(resource: StringResource): List<String> = loadStringArray(resource, DefaultResourceReader) suspend fun getStringArray(resource: StringResource): List<String> =
loadStringArray(resource, DefaultResourceReader, getResourceEnvironment())
private suspend fun loadStringArray(resource: StringResource, resourceReader: ResourceReader): List<String> {
val path = resource.getPathByEnvironment() private suspend fun loadStringArray(
resource: StringResource,
resourceReader: ResourceReader,
environment: ResourceEnvironment
): List<String> {
val path = resource.getPathByEnvironment(environment)
val keyToValue = getParsedStrings(path, resourceReader) val keyToValue = getParsedStrings(path, resourceReader)
val item = keyToValue[resource.key] as? StringItem.Array val item = keyToValue[resource.key] as? StringItem.Array
?: error("String array ID=`${resource.key}` is not found!") ?: error("String array ID=`${resource.key}` is not found!")

120
components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ResourceTest.kt

@ -5,62 +5,98 @@
package org.jetbrains.compose.resources package org.jetbrains.compose.resources
import kotlin.test.Test import org.jetbrains.compose.resources.DensityQualifier.*
import kotlin.test.assertEquals import org.jetbrains.compose.resources.ThemeQualifier.DARK
import kotlin.test.assertFailsWith import org.jetbrains.compose.resources.ThemeQualifier.LIGHT
import kotlin.test.assertNotEquals import kotlin.test.*
@OptIn(ExperimentalResourceApi::class)
class ResourceTest { class ResourceTest {
@Test @Test
fun testResourceEquals() = runBlockingTest { fun testResourceEquals() = runBlockingTest {
assertEquals(ImageResource("a"), ImageResource("a")) assertEquals(DrawableResource("a"), DrawableResource("a"))
} }
@Test @Test
fun testResourceNotEquals() = runBlockingTest { fun testResourceNotEquals() = runBlockingTest {
assertNotEquals(ImageResource("a"), ImageResource("b")) assertNotEquals(DrawableResource("a"), DrawableResource("b"))
} }
@Test @Test
fun testMissingResource() = runBlockingTest { fun testGetPathByEnvironment() {
assertFailsWith<MissingResourceException> { val resource = DrawableResource(
readResourceBytes("missing.png") id = "ImageResource:test",
} items = setOf(
val error = assertFailsWith<IllegalStateException> { ResourceItem(setOf(), "default"),
loadString(TestStringResource("unknown_id")) ResourceItem(setOf(LanguageQualifier("en")), "en"),
} ResourceItem(setOf(LanguageQualifier("en"), RegionQualifier("US"), XHDPI), "en-rUS-xhdpi"),
assertEquals("String ID=`unknown_id` is not found!", error.message) ResourceItem(setOf(LanguageQualifier("fr"), LIGHT), "fr-light"),
} ResourceItem(setOf(DARK), "dark"),
)
@Test )
fun testReadFileResource() = runBlockingTest { fun env(lang: String, reg: String, theme: ThemeQualifier, density: DensityQualifier) = ResourceEnvironment(
val bytes = readResourceBytes("strings.xml") language = LanguageQualifier(lang),
region = RegionQualifier(reg),
theme = theme,
density = density
)
assertEquals( assertEquals(
""" "en-rUS-xhdpi",
<resources> resource.getPathByEnvironment(env("en", "US", DARK, XXHDPI))
<string name="app_name">Compose Resources App</string> )
<string name="hello">😊 Hello world!</string> assertEquals(
<string name="str_template">Hello, %1${'$'}s! You have %2${'$'}d new messages.</string> "en",
<string-array name="str_arr"> resource.getPathByEnvironment(env("en", "IN", LIGHT, LDPI))
<item>item 1</item>
<item>item 2</item>
<item>item 3</item>
</string-array>
</resources>
""".trimIndent(),
bytes.decodeToString()
) )
}
@Test
fun testLoadStringResource() = runBlockingTest {
assertEquals("Compose Resources App", loadString(TestStringResource("app_name")))
assertEquals( assertEquals(
"Hello, test-name! You have 42 new messages.", "default",
loadString(TestStringResource("str_template"), "test-name", 42) 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<IllegalStateException> {
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<IllegalStateException> {
resourceWithFewFiles.getPathByEnvironment(env("en", "US", DARK, XHDPI))
}.message.let { msg ->
assertEquals("Resource with ID='ImageResource:test3' has more than one file: en1, en2", msg)
}
} }
} }

19
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)
)
}

67
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.foundation.Image
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.*
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.runComposeUiTest import androidx.compose.ui.test.runComposeUiTest
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -12,6 +10,7 @@ import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
@OptIn(ExperimentalResourceApi::class, ExperimentalTestApi::class) @OptIn(ExperimentalResourceApi::class, ExperimentalTestApi::class)
class ComposeResourceTest { class ComposeResourceTest {
@ -25,7 +24,7 @@ class ComposeResourceTest {
@Test @Test
fun testCountRecompositions() = runComposeUiTest { fun testCountRecompositions() = runComposeUiTest {
runBlockingTest { runBlockingTest {
val imagePathFlow = MutableStateFlow(ImageResource("1.png")) val imagePathFlow = MutableStateFlow(DrawableResource("1.png"))
val recompositionsCounter = RecompositionsCounter() val recompositionsCounter = RecompositionsCounter()
setContent { setContent {
val res by imagePathFlow.collectAsState() val res by imagePathFlow.collectAsState()
@ -35,7 +34,7 @@ class ComposeResourceTest {
} }
} }
awaitIdle() awaitIdle()
imagePathFlow.emit(ImageResource("2.png")) imagePathFlow.emit(DrawableResource("2.png"))
awaitIdle() awaitIdle()
assertEquals(2, recompositionsCounter.count) assertEquals(2, recompositionsCounter.count)
} }
@ -45,7 +44,7 @@ class ComposeResourceTest {
fun testImageResourceCache() = runComposeUiTest { fun testImageResourceCache() = runComposeUiTest {
runBlockingTest { runBlockingTest {
val testResourceReader = TestResourceReader() val testResourceReader = TestResourceReader()
val imagePathFlow = MutableStateFlow(ImageResource("1.png")) val imagePathFlow = MutableStateFlow(DrawableResource("1.png"))
setContent { setContent {
CompositionLocalProvider(LocalResourceReader provides testResourceReader) { CompositionLocalProvider(LocalResourceReader provides testResourceReader) {
val res by imagePathFlow.collectAsState() val res by imagePathFlow.collectAsState()
@ -53,9 +52,9 @@ class ComposeResourceTest {
} }
} }
awaitIdle() awaitIdle()
imagePathFlow.emit(ImageResource("2.png")) imagePathFlow.emit(DrawableResource("2.png"))
awaitIdle() awaitIdle()
imagePathFlow.emit(ImageResource("1.png")) imagePathFlow.emit(DrawableResource("1.png"))
awaitIdle() awaitIdle()
assertEquals( assertEquals(
@ -73,8 +72,8 @@ class ComposeResourceTest {
setContent { setContent {
CompositionLocalProvider(LocalResourceReader provides testResourceReader) { CompositionLocalProvider(LocalResourceReader provides testResourceReader) {
val res by stringIdFlow.collectAsState() val res by stringIdFlow.collectAsState()
Text(getString(res)) Text(stringResource(res))
Text(getStringArray(TestStringResource("str_arr")).joinToString()) Text(stringArrayResource(TestStringResource("str_arr")).joinToString())
} }
} }
awaitIdle() awaitIdle()
@ -94,14 +93,56 @@ class ComposeResourceTest {
fun testReadStringResource() = runComposeUiTest { fun testReadStringResource() = runComposeUiTest {
runBlockingTest { runBlockingTest {
setContent { setContent {
assertEquals("Compose Resources App", getString(TestStringResource("app_name"))) assertEquals("Compose Resources App", stringResource(TestStringResource("app_name")))
assertEquals( assertEquals(
"Hello, test-name! You have 42 new messages.", "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() 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<MissingResourceException> {
readResourceBytes("missing.png")
}
val error = assertFailsWith<IllegalStateException> {
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(
"""
<resources>
<string name="app_name">Compose Resources App</string>
<string name="hello">😊 Hello world!</string>
<string name="str_template">Hello, %1${'$'}s! You have %2${'$'}d new messages.</string>
<string-array name="str_arr">
<item>item 1</item>
<item>item 2</item>
<item>item 3</item>
</string-array>
</resources>
""".trimIndent(),
bytes.decodeToString()
)
}
} }

21
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)
)
}

4
components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ImageResources.js.kt

@ -1,8 +1,6 @@
package org.jetbrains.compose.resources package org.jetbrains.compose.resources
import org.jetbrains.compose.resources.vector.xmldom.Element import org.jetbrains.compose.resources.vector.xmldom.*
import org.jetbrains.compose.resources.vector.xmldom.ElementImpl
import org.jetbrains.compose.resources.vector.xmldom.MalformedXMLException
import org.w3c.dom.parsing.DOMParser import org.w3c.dom.parsing.DOMParser
internal actual fun ByteArray.toXmlElement(): Element { internal actual fun ByteArray.toXmlElement(): Element {

23
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)
)
}

32
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<NSNumber>(
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)
)
}

8
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.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.platform.Font import androidx.compose.ui.text.platform.Font
import kotlin.io.encoding.Base64 import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
@ -35,8 +33,8 @@ private val defaultEmptyFont by lazy { Font("org.jetbrains.compose.emptyFont", B
@Composable @Composable
actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): Font { actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): Font {
val resourceReader = LocalResourceReader.current val resourceReader = LocalResourceReader.current
val fontFile by rememberState(resource, { defaultEmptyFont }) { val fontFile by rememberResourceState(resource, { defaultEmptyFont }) { env ->
val path = resource.getPathByEnvironment() val path = resource.getPathByEnvironment(env)
val fontBytes = resourceReader.read(path) val fontBytes = resourceReader.read(path)
Font(path, fontBytes, weight, style) Font(path, fontBytes, weight, style)
} }

23
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)
)
}

9
components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/PlatformState.web.kt → 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 import androidx.compose.runtime.remember
@Composable @Composable
internal actual fun <T> rememberState(key: Any, getDefault: () -> T, block: suspend () -> T): State<T> { internal actual fun <T> rememberResourceState(
key: Any,
getDefault: () -> T,
block: suspend (ResourceEnvironment) -> T
): State<T> {
val environment = rememberEnvironment()
val state = remember(key) { mutableStateOf(getDefault()) } val state = remember(key) { mutableStateOf(getDefault()) }
LaunchedEffect(key) { LaunchedEffect(key) {
state.value = block() state.value = block(environment)
} }
return state return state
} }

2
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidTargetConfiguration.kt

@ -25,7 +25,7 @@ internal fun Project.configureAndroidResources(
val copyFonts = registerTask<Copy>("copyFontsToAndroidAssets") { val copyFonts = registerTask<Copy>("copyFontsToAndroidAssets") {
includeEmptyDirs = false includeEmptyDirs = false
from(commonResourcesDir) from(commonResourcesDir)
include("**/fonts/*") include("**/font*/*")
into(androidFontsDir) into(androidFontsDir)
onlyIf { onlyIfProvider.get() } onlyIf { onlyIfProvider.get() }
} }

4
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt

@ -63,11 +63,11 @@ abstract class GenerateResClassTask : DefaultTask() {
val file = this val file = this
if (file.isDirectory) return null if (file.isDirectory) return null
val dirName = file.parentFile.name ?: return null val dirName = file.parentFile.name ?: return null
val typeAndQualifiers = dirName.lowercase().split("-") val typeAndQualifiers = dirName.split("-")
if (typeAndQualifiers.isEmpty()) return null if (typeAndQualifiers.isEmpty()) return null
val typeString = typeAndQualifiers.first().lowercase() 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) val path = file.toPath().relativeTo(relativeTo)
return if (typeString == "values" && file.name.equals("strings.xml", true)) { return if (typeString == "values" && file.name.equals("strings.xml", true)) {

105
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt

@ -1,44 +1,102 @@
package org.jetbrains.compose.resources package org.jetbrains.compose.resources
import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.*
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 java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.invariantSeparatorsPathString import kotlin.io.path.invariantSeparatorsPathString
import kotlin.io.path.pathString
internal enum class ResourceType(val typeName: String) { internal enum class ResourceType(val typeName: String) {
IMAGE("images"), DRAWABLE("drawable"),
STRING("strings"), STRING("string"),
FONT("fonts"); FONT("font");
override fun toString(): String = typeName
companion object { companion object {
fun fromString(str: String) = when (str) { fun fromString(str: String): ResourceType =
"images" -> ResourceType.IMAGE ResourceType.values()
"strings" -> ResourceType.STRING .firstOrNull { it.typeName.equals(str, true) }
"fonts" -> ResourceType.FONT ?: error("Unknown resource type: '$str'.")
else -> error("Unknown resource type: $str")
}
} }
} }
internal data class ResourceItem( internal data class ResourceItem(
val type: ResourceType, val type: ResourceType,
val qualifiers: Set<String>, val qualifiers: List<String>,
val name: String, val name: String,
val path: Path val path: Path
) )
private fun ResourceItem.getClassName(): ClassName = when (type) { 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.STRING -> ClassName("org.jetbrains.compose.resources", "StringResource")
ResourceType.FONT -> ClassName("org.jetbrains.compose.resources", "FontResource") 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<ClassName, String>()
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( internal fun getResFileSpec(
//type -> id -> items //type -> id -> items
resources: Map<ResourceType, Map<String, List<ResourceItem>>>, resources: Map<ResourceType, Map<String, List<ResourceItem>>>,
@ -77,9 +135,12 @@ private fun TypeSpec.Builder.addResourceProperty(name: String, items: List<Resou
} }
add("setOf(\n").withIndent { add("setOf(\n").withIndent {
items.forEach { item -> items.forEach { item ->
val qualifiers = item.qualifiers.sorted().joinToString { "\"$it\"" } add("%T(\n", resourceItemClass).withIndent {
//file separator should be '/' on all platforms add("setOf(").addQualifiers(item).add("),\n")
add("%T(setOf($qualifiers), \"${item.path.invariantSeparatorsPathString}\"),\n", resourceItemClass) //file separator should be '/' on all platforms
add("\"${item.path.invariantSeparatorsPathString}\"\n")
}
add("),\n")
} }
} }
add(")\n") add(")\n")

54
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") file("expected/Res.kt")
) )
check.logContains(""" check.logContains("""
Unknown resource type: ignored Unknown resource type: 'ignored'.
""".trimIndent()) """.trimIndent())
} }
file("src/commonMain/resources/composeRes/images/vector_2.xml").renameTo( file("src/commonMain/resources/composeRes/drawable/vector_2.xml").renameTo(
file("src/commonMain/resources/composeRes/images/vector_3.xml") file("src/commonMain/resources/composeRes/drawable/vector_3.xml")
) )
//check resource's accessors were regenerated //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/drawable-en").renameTo(
file("src/commonMain/resources/composeRes/images/vector_2.xml") 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 //TODO: check a real build after a release a new version of the resources library

99
gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt

@ -1,84 +1,123 @@
package app.group.generated.resources package app.group.generated.resources
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.FontResource 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.ResourceItem
import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.ThemeQualifier
internal object Res { internal object Res {
public object fonts { public object drawable {
public val emptyfont: FontResource = FontResource( public val _3_strange_name: DrawableResource = DrawableResource(
"FONT:emptyfont", "drawable:_3_strange_name",
setOf( setOf(
ResourceItem(setOf(), "composeRes/fonts/emptyFont.otf"), ResourceItem(
setOf(),
"composeRes/drawable/3-strange-name.xml"
),
) )
) )
}
public object images { public val vector: DrawableResource = DrawableResource(
public val _3_strange_name: ImageResource = ImageResource( "drawable:vector",
"IMAGE:_3_strange_name",
setOf( 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( public val vector_2: DrawableResource = DrawableResource(
"IMAGE:vector", "drawable:vector_2",
setOf( setOf(
ResourceItem(setOf("q1", "q2"), "composeRes/images-q1-q2/vector.xml"), ResourceItem(
ResourceItem(setOf("q1"), "composeRes/images-q1/vector.xml"), setOf(),
ResourceItem(setOf("q2"), "composeRes/images-q2/vector.xml"), "composeRes/drawable/vector_2.xml"
ResourceItem(setOf(), "composeRes/images/vector.xml"), ),
) )
) )
}
public val vector_2: ImageResource = ImageResource( public object font {
"IMAGE:vector_2", public val emptyfont: FontResource = FontResource(
"font:emptyfont",
setOf( 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( public val app_name: StringResource = StringResource(
"STRING:app_name", "string:app_name",
"app_name", "app_name",
setOf( setOf(
ResourceItem(setOf(), "composeRes/values/strings.xml"), ResourceItem(
setOf(),
"composeRes/values/strings.xml"
),
) )
) )
public val hello: StringResource = StringResource( public val hello: StringResource = StringResource(
"STRING:hello", "string:hello",
"hello", "hello",
setOf( setOf(
ResourceItem(setOf(), "composeRes/values/strings.xml"), ResourceItem(
setOf(),
"composeRes/values/strings.xml"
),
) )
) )
public val multi_line: StringResource = StringResource( public val multi_line: StringResource = StringResource(
"STRING:multi_line", "string:multi_line",
"multi_line", "multi_line",
setOf( setOf(
ResourceItem(setOf(), "composeRes/values/strings.xml"), ResourceItem(
setOf(),
"composeRes/values/strings.xml"
),
) )
) )
public val str_arr: StringResource = StringResource( public val str_arr: StringResource = StringResource(
"STRING:str_arr", "string:str_arr",
"str_arr", "str_arr",
setOf( setOf(
ResourceItem(setOf(), "composeRes/values/strings.xml"), ResourceItem(
setOf(),
"composeRes/values/strings.xml"
),
) )
) )
public val str_template: StringResource = StringResource( public val str_template: StringResource = StringResource(
"STRING:str_template", "string:str_template",
"str_template", "str_template",
setOf( setOf(
ResourceItem(setOf(), "composeRes/values/strings.xml"), ResourceItem(
setOf(),
"composeRes/values/strings.xml"
),
) )
) )
} }

0
gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q1-q2/vector.xml → gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable-au-rUS/vector.xml

0
gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q1/vector.xml → gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable-dark-ge/vector.xml

0
gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q2/vector.xml → gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable-en/vector.xml

0
gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images/3-strange-name.xml → gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable/3-strange-name.xml

0
gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images/vector.xml → gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable/vector.xml

0
gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images/vector_2.xml → gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/drawable/vector_2.xml

0
gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/fonts/emptyFont.otf → gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/font/emptyFont.otf

Loading…
Cancel
Save