Browse Source

Resources gradle plugin (#3961)

dima.avdeev/cocoapods-dynamic-frameworks
Konstantin 6 months ago committed by GitHub
parent
commit
181bfd1612
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/workflows/gradle-plugin.yml
  2. 1
      components/gradle.properties
  3. 5
      components/resources/demo/shared/build.gradle.kts
  4. 6
      components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt
  5. 5
      components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FontRes.kt
  6. 33
      components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/ImagesRes.kt
  7. 25
      components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt
  8. 0
      components/resources/demo/shared/src/commonMain/resources/composeRes/fonts/font_awesome.otf
  9. 0
      components/resources/demo/shared/src/commonMain/resources/composeRes/images/compose.png
  10. 0
      components/resources/demo/shared/src/commonMain/resources/composeRes/images/droid_icon.xml
  11. 0
      components/resources/demo/shared/src/commonMain/resources/composeRes/images/insta_icon.xml
  12. 0
      components/resources/demo/shared/src/commonMain/resources/composeRes/images/land.webp
  13. 0
      components/resources/demo/shared/src/commonMain/resources/composeRes/values/strings.xml
  14. BIN
      components/resources/demo/shared/src/commonMain/resources/font_awesome.otf
  15. 38
      components/resources/library/src/androidInstrumentedTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.android.kt
  16. 5
      components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt
  17. 29
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt
  18. 79
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt
  19. 44
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.kt
  20. 14
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceIndex.kt
  21. 112
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt
  22. 12
      components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ResourceTest.kt
  23. 6
      components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt
  24. 38
      components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.desktop.kt
  25. 9
      components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt
  26. 3
      gradle-plugins/build.gradle.kts
  27. 11
      gradle-plugins/compose/build.gradle.kts
  28. 5
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt
  29. 8
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt
  30. 18
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/IosGradleProperties.kt
  31. 42
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidTargetConfiguration.kt
  32. 98
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt
  33. 77
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesGenerator.kt
  34. 95
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt
  35. 2
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/IosTargetResources.kt
  36. 5
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/SyncComposeResourcesForIosTask.kt
  37. 11
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/configureSyncIosResources.kt
  38. 2
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/determineIosKonanTargetsFromEnv.kt
  39. 46
      gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt
  40. 2
      gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProject.kt
  41. 2
      gradle-plugins/compose/src/test/test-projects/application/mpp/settings.gradle
  42. 39
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/build.gradle.kts
  43. 78
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt
  44. 1
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/gradle.properties
  45. 22
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/settings.gradle.kts
  46. 20
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/kotlin/App.kt
  47. BIN
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/fonts/emptyFont.otf
  48. 0
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/ignored/ignored_3.txt
  49. 0
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/ignored_1.txt
  50. 36
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q1-q2/vector.xml
  51. 36
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q1/vector.xml
  52. 36
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q2/vector.xml
  53. 36
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images/vector.xml
  54. 36
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images/vector_2.xml
  55. 13
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/values/strings.xml
  56. 0
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/ignored_2.txt
  57. 2
      gradle-plugins/gradle.properties
  58. 11
      gradle-plugins/gradle/libs.versions.toml

2
.github/workflows/gradle-plugin.yml

@ -10,7 +10,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-20.04, macos-12, windows-2022] os: [ubuntu-20.04, macos-12, windows-2022]
gradle: [7.3.3, 8.3] gradle: [7.4, 8.3]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

1
components/gradle.properties

@ -15,6 +15,7 @@ agp.version=8.1.2
org.jetbrains.compose.experimental.jscanvas.enabled=true org.jetbrains.compose.experimental.jscanvas.enabled=true
org.jetbrains.compose.experimental.macos.enabled=true org.jetbrains.compose.experimental.macos.enabled=true
org.jetbrains.compose.experimental.uikit.enabled=true org.jetbrains.compose.experimental.uikit.enabled=true
compose.resources.always.generate.accessors=true
compose.desktop.verbose=true compose.desktop.verbose=true
compose.useMavenLocal=false compose.useMavenLocal=false

5
components/resources/demo/shared/build.gradle.kts

@ -78,11 +78,6 @@ android {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
} }
sourceSets {
named("main") {
resources.srcDir("src/commonMain/resources")
}
}
} }
compose.experimental { compose.experimental {

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: 'images/droid_icon.xml'", text = "File: 'composeRes/images/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("images/droid_icon.xml") bytes = readResourceBytes("composeRes/images/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("images/droid_icon.xml") bytes = readBytes("composeRes/images/droid_icon.xml")
} }
Text(bytes.decodeToString()) Text(bytes.decodeToString())
""".trimIndent() """.trimIndent()

5
components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FontRes.kt

@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import components.resources.demo.generated.resources.Res
import org.jetbrains.compose.resources.Font import org.jetbrains.compose.resources.Font
@Composable @Composable
@ -28,7 +29,7 @@ fun FontRes(paddingValues: PaddingValues) {
Text( Text(
modifier = Modifier.padding(8.dp), modifier = Modifier.padding(8.dp),
text = """ text = """
val fontAwesome = FontFamily(Font("font_awesome.otf")) val fontAwesome = FontFamily(Font(Res.fonts.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),
@ -42,7 +43,7 @@ fun FontRes(paddingValues: PaddingValues) {
) )
} }
val fontAwesome = FontFamily(Font("font_awesome.otf")) val fontAwesome = FontFamily(Font(Res.fonts.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),

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

@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
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 org.jetbrains.compose.resources.imageResource import org.jetbrains.compose.resources.imageResource
import org.jetbrains.compose.resources.vectorResource import org.jetbrains.compose.resources.vectorResource
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
@ -27,13 +28,13 @@ fun ImagesRes(contentPadding: PaddingValues) {
) { ) {
Image( Image(
modifier = Modifier.size(100.dp), modifier = Modifier.size(100.dp),
painter = painterResource("images/compose.png"), painter = painterResource(Res.images.compose),
contentDescription = null contentDescription = null
) )
Text( Text(
""" """
Image( Image(
painter = painterResource("images/compose.png") painter = painterResource(Res.images.compose)
) )
""".trimIndent() """.trimIndent()
) )
@ -46,13 +47,13 @@ fun ImagesRes(contentPadding: PaddingValues) {
) { ) {
Image( Image(
modifier = Modifier.size(100.dp), modifier = Modifier.size(100.dp),
painter = painterResource("images/insta_icon.xml"), painter = painterResource(Res.images.insta_icon),
contentDescription = null contentDescription = null
) )
Text( Text(
""" """
Image( Image(
painter = painterResource("images/insta_icon.xml") painter = painterResource(Res.images.insta_icon)
) )
""".trimIndent() """.trimIndent()
) )
@ -65,13 +66,13 @@ fun ImagesRes(contentPadding: PaddingValues) {
) { ) {
Image( Image(
modifier = Modifier.size(140.dp), modifier = Modifier.size(140.dp),
bitmap = imageResource("images/land.webp"), bitmap = imageResource(Res.images.land),
contentDescription = null contentDescription = null
) )
Text( Text(
""" """
Image( Image(
bitmap = imageResource("images/land.webp") bitmap = imageResource(Res.images.land)
) )
""".trimIndent() """.trimIndent()
) )
@ -84,13 +85,13 @@ fun ImagesRes(contentPadding: PaddingValues) {
) { ) {
Image( Image(
modifier = Modifier.size(100.dp), modifier = Modifier.size(100.dp),
imageVector = vectorResource("images/droid_icon.xml"), imageVector = vectorResource(Res.images.droid_icon),
contentDescription = null contentDescription = null
) )
Text( Text(
""" """
Image( Image(
imageVector = vectorResource("images/droid_icon.xml") imageVector = vectorResource(Res.images.droid_icon)
) )
""".trimIndent() """.trimIndent()
) )
@ -103,13 +104,13 @@ fun ImagesRes(contentPadding: PaddingValues) {
) { ) {
Icon( Icon(
modifier = Modifier.size(100.dp), modifier = Modifier.size(100.dp),
painter = painterResource("images/compose.png"), painter = painterResource(Res.images.compose),
contentDescription = null contentDescription = null
) )
Text( Text(
""" """
Icon( Icon(
painter = painterResource("images/compose.png") painter = painterResource(Res.images.compose)
) )
""".trimIndent() """.trimIndent()
) )
@ -122,13 +123,13 @@ fun ImagesRes(contentPadding: PaddingValues) {
) { ) {
Icon( Icon(
modifier = Modifier.size(100.dp), modifier = Modifier.size(100.dp),
painter = painterResource("images/insta_icon.xml"), painter = painterResource(Res.images.insta_icon),
contentDescription = null contentDescription = null
) )
Text( Text(
""" """
Icon( Icon(
painter = painterResource("images/insta_icon.xml") painter = painterResource(Res.images.insta_icon)
) )
""".trimIndent() """.trimIndent()
) )
@ -141,13 +142,13 @@ fun ImagesRes(contentPadding: PaddingValues) {
) { ) {
Icon( Icon(
modifier = Modifier.size(140.dp), modifier = Modifier.size(140.dp),
bitmap = imageResource("images/land.webp"), bitmap = imageResource(Res.images.land),
contentDescription = null contentDescription = null
) )
Text( Text(
""" """
Icon( Icon(
bitmap = imageResource("images/land.webp") bitmap = imageResource(Res.images.land)
) )
""".trimIndent() """.trimIndent()
) )
@ -160,13 +161,13 @@ fun ImagesRes(contentPadding: PaddingValues) {
) { ) {
Icon( Icon(
modifier = Modifier.size(100.dp), modifier = Modifier.size(100.dp),
imageVector = vectorResource("images/droid_icon.xml"), imageVector = vectorResource(Res.images.droid_icon),
contentDescription = null contentDescription = null
) )
Text( Text(
""" """
Icon( Icon(
imageVector = vectorResource("images/droid_icon.xml") imageVector = vectorResource(Res.images.droid_icon)
) )
""".trimIndent() """.trimIndent()
) )

25
components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt

@ -21,6 +21,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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 org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.getStringArray import org.jetbrains.compose.resources.getStringArray
import org.jetbrains.compose.resources.readResourceBytes import org.jetbrains.compose.resources.readResourceBytes
@ -32,7 +33,7 @@ fun StringRes(paddingValues: PaddingValues) {
) { ) {
Text( Text(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
text = "strings.xml", text = "composeRes/values/strings.xml",
style = MaterialTheme.typography.titleLarge style = MaterialTheme.typography.titleLarge
) )
OutlinedCard( OutlinedCard(
@ -42,7 +43,7 @@ fun StringRes(paddingValues: PaddingValues) {
) { ) {
var bytes by remember { mutableStateOf(ByteArray(0)) } var bytes by remember { mutableStateOf(ByteArray(0)) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
bytes = readResourceBytes("strings.xml") bytes = readResourceBytes("composeRes/values/strings.xml")
} }
Text( Text(
modifier = Modifier.padding(8.dp), modifier = Modifier.padding(8.dp),
@ -53,9 +54,9 @@ fun StringRes(paddingValues: PaddingValues) {
} }
OutlinedTextField( OutlinedTextField(
modifier = Modifier.padding(16.dp).fillMaxWidth(), modifier = Modifier.padding(16.dp).fillMaxWidth(),
value = getString("app_name"), value = getString(Res.strings.app_name),
onValueChange = {}, onValueChange = {},
label = { Text("Text(getString(\"app_name\"))") }, label = { Text("Text(getString(Res.strings.app_name)") },
enabled = false, enabled = false,
colors = TextFieldDefaults.colors( colors = TextFieldDefaults.colors(
disabledTextColor = MaterialTheme.colorScheme.onSurface, disabledTextColor = MaterialTheme.colorScheme.onSurface,
@ -65,9 +66,9 @@ fun StringRes(paddingValues: PaddingValues) {
) )
OutlinedTextField( OutlinedTextField(
modifier = Modifier.padding(16.dp).fillMaxWidth(), modifier = Modifier.padding(16.dp).fillMaxWidth(),
value = getString("hello"), value = getString(Res.strings.hello),
onValueChange = {}, onValueChange = {},
label = { Text("Text(getString(\"hello\"))") }, label = { Text("Text(getString(Res.strings.hello)") },
enabled = false, enabled = false,
colors = TextFieldDefaults.colors( colors = TextFieldDefaults.colors(
disabledTextColor = MaterialTheme.colorScheme.onSurface, disabledTextColor = MaterialTheme.colorScheme.onSurface,
@ -77,9 +78,9 @@ fun StringRes(paddingValues: PaddingValues) {
) )
OutlinedTextField( OutlinedTextField(
modifier = Modifier.padding(16.dp).fillMaxWidth(), modifier = Modifier.padding(16.dp).fillMaxWidth(),
value = getString("multi_line"), value = getString(Res.strings.multi_line),
onValueChange = {}, onValueChange = {},
label = { Text("Text(getString(\"multi_line\"))") }, label = { Text("Text(getString(Res.strings.multi_line)") },
enabled = false, enabled = false,
colors = TextFieldDefaults.colors( colors = TextFieldDefaults.colors(
disabledTextColor = MaterialTheme.colorScheme.onSurface, disabledTextColor = MaterialTheme.colorScheme.onSurface,
@ -89,9 +90,9 @@ fun StringRes(paddingValues: PaddingValues) {
) )
OutlinedTextField( OutlinedTextField(
modifier = Modifier.padding(16.dp).fillMaxWidth(), modifier = Modifier.padding(16.dp).fillMaxWidth(),
value = getString("str_template", "User_name", 100), value = getString(Res.strings.str_template, "User_name", 100),
onValueChange = {}, onValueChange = {},
label = { Text("Text(getString(\"str_template\", \"User_name\", 100))") }, label = { Text("Text(getString(Res.strings.str_template, \"User_name\", 100)") },
enabled = false, enabled = false,
colors = TextFieldDefaults.colors( colors = TextFieldDefaults.colors(
disabledTextColor = MaterialTheme.colorScheme.onSurface, disabledTextColor = MaterialTheme.colorScheme.onSurface,
@ -101,9 +102,9 @@ fun StringRes(paddingValues: PaddingValues) {
) )
OutlinedTextField( OutlinedTextField(
modifier = Modifier.padding(16.dp).fillMaxWidth(), modifier = Modifier.padding(16.dp).fillMaxWidth(),
value = getStringArray("str_arr").toString(), value = getStringArray(Res.strings.str_arr).toString(),
onValueChange = {}, onValueChange = {},
label = { Text("Text(getStringArray(\"str_arr\").toString())") }, label = { Text("Text(getStringArray(Res.strings.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/androidMain/assets/font_awesome.otf → components/resources/demo/shared/src/commonMain/resources/composeRes/fonts/font_awesome.otf

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

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

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

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

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

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

0
components/resources/demo/shared/src/commonMain/resources/strings.xml → components/resources/demo/shared/src/commonMain/resources/composeRes/values/strings.xml

BIN
components/resources/demo/shared/src/commonMain/resources/font_awesome.otf

Binary file not shown.

38
components/resources/library/src/androidInstrumentedTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.android.kt

@ -25,17 +25,17 @@ class ComposeResourceTest {
@Test @Test
fun testCountRecompositions() = runComposeUiTest { fun testCountRecompositions() = runComposeUiTest {
runBlockingTest { runBlockingTest {
val imagePathFlow = MutableStateFlow("1.png") val imagePathFlow = MutableStateFlow(ImageResource("1.png"))
val recompositionsCounter = RecompositionsCounter() val recompositionsCounter = RecompositionsCounter()
setContent { setContent {
val path by imagePathFlow.collectAsState() val res by imagePathFlow.collectAsState()
val res = imageResource(path) val imgRes = imageResource(res)
recompositionsCounter.content { recompositionsCounter.content {
Image(bitmap = res, contentDescription = null) Image(bitmap = imgRes, contentDescription = null)
} }
} }
awaitIdle() awaitIdle()
imagePathFlow.emit("2.png") imagePathFlow.emit(ImageResource("2.png"))
awaitIdle() awaitIdle()
assertEquals(2, recompositionsCounter.count) assertEquals(2, recompositionsCounter.count)
} }
@ -45,17 +45,17 @@ class ComposeResourceTest {
fun testImageResourceCache() = runComposeUiTest { fun testImageResourceCache() = runComposeUiTest {
runBlockingTest { runBlockingTest {
val testResourceReader = TestResourceReader() val testResourceReader = TestResourceReader()
val imagePathFlow = MutableStateFlow("1.png") val imagePathFlow = MutableStateFlow(ImageResource("1.png"))
setContent { setContent {
CompositionLocalProvider(LocalResourceReader provides testResourceReader) { CompositionLocalProvider(LocalResourceReader provides testResourceReader) {
val path by imagePathFlow.collectAsState() val res by imagePathFlow.collectAsState()
Image(painterResource(path), null) Image(painterResource(res), null)
} }
} }
awaitIdle() awaitIdle()
imagePathFlow.emit("2.png") imagePathFlow.emit(ImageResource("2.png"))
awaitIdle() awaitIdle()
imagePathFlow.emit("1.png") imagePathFlow.emit(ImageResource("1.png"))
awaitIdle() awaitIdle()
assertEquals( assertEquals(
@ -69,18 +69,18 @@ class ComposeResourceTest {
fun testStringResourceCache() = runComposeUiTest { fun testStringResourceCache() = runComposeUiTest {
runBlockingTest { runBlockingTest {
val testResourceReader = TestResourceReader() val testResourceReader = TestResourceReader()
val stringIdFlow = MutableStateFlow("app_name") val stringIdFlow = MutableStateFlow(TestStringResource("app_name"))
setContent { setContent {
CompositionLocalProvider(LocalResourceReader provides testResourceReader) { CompositionLocalProvider(LocalResourceReader provides testResourceReader) {
val textId by stringIdFlow.collectAsState() val res by stringIdFlow.collectAsState()
Text(getString(textId)) Text(getString(res))
Text(getStringArray("str_arr").joinToString()) Text(getStringArray(TestStringResource("str_arr")).joinToString())
} }
} }
awaitIdle() awaitIdle()
stringIdFlow.emit("hello") stringIdFlow.emit(TestStringResource("hello"))
awaitIdle() awaitIdle()
stringIdFlow.emit("app_name") stringIdFlow.emit(TestStringResource("app_name"))
awaitIdle() awaitIdle()
assertEquals( assertEquals(
@ -94,12 +94,12 @@ class ComposeResourceTest {
fun testReadStringResource() = runComposeUiTest { fun testReadStringResource() = runComposeUiTest {
runBlockingTest { runBlockingTest {
setContent { setContent {
assertEquals("Compose Resources App", getString("app_name")) assertEquals("Compose Resources App", getString(TestStringResource("app_name")))
assertEquals( assertEquals(
"Hello, test-name! You have 42 new messages.", "Hello, test-name! You have 42 new messages.",
getString("str_template", "test-name", 42) getString(TestStringResource("str_template"), "test-name", 42)
) )
assertEquals(listOf("item 1", "item 2", "item 3"), getStringArray("str_arr")) assertEquals(listOf("item 1", "item 2", "item 3"), getStringArray(TestStringResource("str_arr")))
} }
awaitIdle() awaitIdle()
} }

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

@ -1,7 +1,6 @@
package org.jetbrains.compose.resources package org.jetbrains.compose.resources
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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.Font
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
@ -9,7 +8,7 @@ import androidx.compose.ui.text.font.FontWeight
@ExperimentalResourceApi @ExperimentalResourceApi
@Composable @Composable
actual fun Font(id: ResourceId, weight: FontWeight, style: FontStyle): Font { actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): Font {
val path by rememberState(id, { "" }) { getPathById(id) } val path = resource.getPathByEnvironment()
return Font(path, LocalContext.current.assets, weight, style) return Font(path, LocalContext.current.assets, weight, style)
} }

29
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt

@ -1,14 +1,37 @@
package org.jetbrains.compose.resources package org.jetbrains.compose.resources
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
/** /**
* Creates a font with the specified resource ID, weight, and style. * Represents a font resource.
* *
* @param id The resource ID of the font. * @param id The identifier of the font resource.
* @param items The set of resource items associated with the font resource.
*
* @see Resource
*/
@Immutable
class FontResource(id: String, items: Set<ResourceItem>): Resource(id, items)
/**
* Creates an [FontResource] object with the specified path.
*
* @param path The path to the font resource file.
* @return A new [FontResource] object.
*/
fun FontResource(path: String): FontResource = FontResource(
id = "FontResource:$path",
items = setOf(ResourceItem(emptySet(), path))
)
/**
* Creates a font using the specified font resource, weight, and style.
*
* @param resource The font resource to be used.
* @param weight The weight of the font. Default value is [FontWeight.Normal]. * @param weight The weight of the font. Default value is [FontWeight.Normal].
* @param style The style of the font. Default value is [FontStyle.Normal]. * @param style The style of the font. Default value is [FontStyle.Normal].
* *
@ -19,7 +42,7 @@ import androidx.compose.ui.text.font.FontWeight
@ExperimentalResourceApi @ExperimentalResourceApi
@Composable @Composable
expect fun Font( expect fun Font(
id: ResourceId, resource: FontResource,
weight: FontWeight = FontWeight.Normal, weight: FontWeight = FontWeight.Normal,
style: FontStyle = FontStyle.Normal style: FontStyle = FontStyle.Normal
): Font ): Font

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

@ -1,7 +1,9 @@
package org.jetbrains.compose.resources package org.jetbrains.compose.resources
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue 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
@ -9,46 +11,68 @@ 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.Dispatchers import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Deferred
import kotlinx.coroutines.withContext import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.sync.Mutex
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.
*
* @param id The unique identifier of the image resource.
* @param items The set of resource items associated with the image resource.
*/
@Immutable
class ImageResource(id: String, items: Set<ResourceItem>) : Resource(id, items)
/**
* Creates an [ImageResource] object with the specified path.
*
* @param path The path of the image resource.
* @return An [ImageResource] object.
*/
fun ImageResource(path: String): ImageResource = ImageResource(
id = "ImageResource:$path",
items = setOf(ResourceItem(emptySet(), path))
)
/** /**
* Retrieves a [Painter] for the given [ResourceId]. * Retrieves a [Painter] using the specified image 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 id The ID of the resource to retrieve the [Painter] from. * @param resource The image resource to be used.
* @return The [Painter] loaded from the resource. * @return The [Painter] loaded from the resource.
*/ */
@ExperimentalResourceApi @ExperimentalResourceApi
@Composable @Composable
fun painterResource(id: ResourceId): Painter { fun painterResource(resource: ImageResource): Painter {
val filePath by rememberFilePath(id) val filePath = remember(resource) { resource.getPathByEnvironment() }
val isXml = filePath.endsWith(".xml", true) val isXml = filePath.endsWith(".xml", true)
if (isXml) { if (isXml) {
return rememberVectorPainter(vectorResource(id)) return rememberVectorPainter(vectorResource(resource))
} else { } else {
return BitmapPainter(imageResource(id)) return BitmapPainter(imageResource(resource))
} }
} }
private val emptyImageBitmap: ImageBitmap by lazy { ImageBitmap(1, 1) } private val emptyImageBitmap: ImageBitmap by lazy { ImageBitmap(1, 1) }
/** /**
* Retrieves an ImageBitmap for the given resource ID. * Retrieves an ImageBitmap using the specified image resource.
* *
* @param id The ID of the resource to load the ImageBitmap from. * @param resource The image resource to be used.
* @return The ImageBitmap loaded from the resource. * @return The ImageBitmap loaded from the resource.
*/ */
@ExperimentalResourceApi @ExperimentalResourceApi
@Composable @Composable
fun imageResource(id: ResourceId): ImageBitmap { fun imageResource(resource: ImageResource): ImageBitmap {
val resourceReader = LocalResourceReader.current val resourceReader = LocalResourceReader.current
val imageBitmap by rememberState(id, { emptyImageBitmap }) { val imageBitmap by rememberState(resource, { emptyImageBitmap }) {
val path = getPathById(id) val path = resource.getPathByEnvironment()
val cached = loadImage(path, resourceReader) { val cached = loadImage(path, resourceReader) {
ImageCache.Bitmap(it.toImageBitmap()) ImageCache.Bitmap(it.toImageBitmap())
} as ImageCache.Bitmap } as ImageCache.Bitmap
@ -62,18 +86,18 @@ private val emptyImageVector: ImageVector by lazy {
} }
/** /**
* Retrieves an ImageVector for the given resource ID. * Retrieves an ImageVector using the specified image resource.
* *
* @param id The ID of the resource to load the ImageVector from. * @param resource The image resource to be used.
* @return The ImageVector loaded from the resource. * @return The ImageVector loaded from the resource.
*/ */
@ExperimentalResourceApi @ExperimentalResourceApi
@Composable @Composable
fun vectorResource(id: ResourceId): ImageVector { fun vectorResource(resource: ImageResource): ImageVector {
val resourceReader = LocalResourceReader.current val resourceReader = LocalResourceReader.current
val density = LocalDensity.current val density = LocalDensity.current
val imageVector by rememberState(id, { emptyImageVector }) { val imageVector by rememberState(resource, { emptyImageVector }) {
val path = getPathById(id) val path = resource.getPathByEnvironment()
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
@ -90,9 +114,8 @@ private sealed interface ImageCache {
class Vector(val vector: ImageVector) : ImageCache class Vector(val vector: ImageVector) : ImageCache
} }
@OptIn(ExperimentalCoroutinesApi::class) private val imageCacheMutex = Mutex()
private val imageCacheDispatcher = Dispatchers.Default.limitedParallelism(1) private val imageCache = mutableMapOf<String, Deferred<ImageCache>>()
private val imageCache = mutableMapOf<String, ImageCache>()
//@TestOnly //@TestOnly
internal fun dropImageCache() { internal fun dropImageCache() {
@ -103,6 +126,14 @@ private suspend fun loadImage(
path: String, path: String,
resourceReader: ResourceReader, resourceReader: ResourceReader,
decode: (ByteArray) -> ImageCache decode: (ByteArray) -> ImageCache
): ImageCache = withContext(imageCacheDispatcher) { ): ImageCache = coroutineScope {
imageCache.getOrPut(path) { decode(resourceReader.read(path)) } val deferred = imageCacheMutex.withLock {
imageCache.getOrPut(path) {
//LAZY - to free the mutex lock as fast as possible
async(start = CoroutineStart.LAZY) {
decode(resourceReader.read(path))
}
}
}
deferred.await()
} }

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

@ -1,6 +1,48 @@
package org.jetbrains.compose.resources package org.jetbrains.compose.resources
internal typealias ResourceId = String import androidx.compose.runtime.Immutable
@RequiresOptIn("This API is experimental and is likely to change in the future.") @RequiresOptIn("This API is experimental and is likely to change in the future.")
annotation class ExperimentalResourceApi annotation class ExperimentalResourceApi
/**
* Represents a resource with an ID and a set of resource items.
*
* @property id The ID of the resource.
* @property items The set of resource items associated with the resource.
*/
@Immutable
sealed class Resource(
internal val id: String,
internal val items: Set<ResourceItem>
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as Resource
return id == other.id
}
override fun hashCode(): Int {
return id.hashCode()
}
}
/**
* Represents a resource item with qualifiers and a path.
*
* @property qualifiers The qualifiers of the resource item.
* @property path The path of the resource item.
*/
@Immutable
data class ResourceItem(
internal val qualifiers: Set<String>,
internal val path: String
)
internal fun Resource.getPathByEnvironment(): String {
//TODO
return items.first().path
}

14
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceIndex.kt

@ -1,14 +0,0 @@
package org.jetbrains.compose.resources
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
//TODO Here will be logic to map a static ID to a file path in resources dir
//at the moment ID = file path
internal suspend fun getPathById(id: ResourceId): String = id
@Composable
internal fun rememberFilePath(id: ResourceId): State<String> =
rememberState(id, { "" }) { getPathById(id) }
internal val ResourceId.stringKey get() = this

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

@ -1,33 +1,58 @@
package org.jetbrains.compose.resources package org.jetbrains.compose.resources
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Deferred
import kotlinx.coroutines.withContext import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.jetbrains.compose.resources.vector.xmldom.Element import org.jetbrains.compose.resources.vector.xmldom.Element
import org.jetbrains.compose.resources.vector.xmldom.NodeList import org.jetbrains.compose.resources.vector.xmldom.NodeList
private const val STRINGS_XML = "strings.xml" //todo
private val SimpleStringFormatRegex = Regex("""%(\d)\$[ds]""") private val SimpleStringFormatRegex = Regex("""%(\d)\$[ds]""")
/**
* Represents a string resource in the application.
*
* @param id The unique identifier of the resource.
* @param key The key used to retrieve the string resource.
* @param items The set of resource items associated with the string resource.
*/
@Immutable
class StringResource(id: String, val key: String, items: Set<ResourceItem>): Resource(id, items)
private sealed interface StringItem { private sealed interface StringItem {
data class Value(val text: String) : StringItem data class Value(val text: String) : StringItem
data class Array(val items: List<String>) : StringItem data class Array(val items: List<String>) : StringItem
} }
@OptIn(ExperimentalCoroutinesApi::class) private val stringsCacheMutex = Mutex()
private val stringsCacheDispatcher = Dispatchers.Default.limitedParallelism(1) private val parsedStringsCache = mutableMapOf<String, Deferred<Map<String, StringItem>>>()
private val parsedStringsCache = mutableMapOf<String, Map<String, StringItem>>()
//@TestOnly //@TestOnly
internal fun dropStringsCache() { internal fun dropStringsCache() {
parsedStringsCache.clear() parsedStringsCache.clear()
} }
private suspend fun getParsedStrings(path: String, resourceReader: ResourceReader): Map<String, StringItem> = private suspend fun getParsedStrings(
withContext(stringsCacheDispatcher) { path: String,
resourceReader: ResourceReader
): Map<String, StringItem> = coroutineScope {
val deferred = stringsCacheMutex.withLock {
parsedStringsCache.getOrPut(path) { parsedStringsCache.getOrPut(path) {
//LAZY - to free the mutex lock as fast as possible
async(start = CoroutineStart.LAZY) {
parseStringXml(path, resourceReader)
}
}
}
deferred.await()
}
private suspend fun parseStringXml(path: String, resourceReader: ResourceReader): Map<String, StringItem> {
val nodes = resourceReader.read(path).toXmlElement().childNodes val nodes = resourceReader.read(path).toXmlElement().childNodes
val strings = nodes.getElementsWithName("string").associate { element -> val strings = nodes.getElementsWithName("string").associate { element ->
element.getAttribute("name") to StringItem.Value(element.textContent.orEmpty()) element.getAttribute("name") to StringItem.Value(element.textContent.orEmpty())
@ -38,48 +63,48 @@ private suspend fun getParsedStrings(path: String, resourceReader: ResourceReade
} }
arrayElement.getAttribute("name") to StringItem.Array(items) arrayElement.getAttribute("name") to StringItem.Array(items)
} }
strings + arrays return strings + arrays
}
} }
/** /**
* Retrieves a string resource using the provided ID. * Retrieves a string using the specified string resource.
* *
* @param id The ID of the string resource to retrieve. * @param resource The string resource to be used.
* @return The retrieved string resource. * @return The retrieved string resource.
* *
* @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
@Composable @Composable
fun getString(id: ResourceId): String { fun getString(resource: StringResource): String {
val resourceReader = LocalResourceReader.current val resourceReader = LocalResourceReader.current
val str by rememberState(id, { "" }) { loadString(id, resourceReader) } val str by rememberState(resource, { "" }) { loadString(resource, resourceReader) }
return str return str
} }
/** /**
* Loads a string resource using the provided ID. * Loads a string using the specified string resource.
* *
* @param id The ID of the string resource to load. * @param resource The string resource to be used.
* @return The loaded string resource. * @return The loaded string resource.
* *
* @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(id: ResourceId): String = loadString(id, DefaultResourceReader) suspend fun loadString(resource: StringResource): String = loadString(resource, DefaultResourceReader)
private suspend fun loadString(id: ResourceId, resourceReader: ResourceReader): String { private suspend fun loadString(resource: StringResource, resourceReader: ResourceReader): String {
val nameToValue = getParsedStrings(getPathById(STRINGS_XML), resourceReader) val path = resource.getPathByEnvironment()
val item = nameToValue[id.stringKey] as? StringItem.Value val keyToValue = getParsedStrings(path, resourceReader)
?: error("String ID=`${id.stringKey}` is not found!") val item = keyToValue[resource.key] as? StringItem.Value
?: error("String ID=`${resource.key}` is not found!")
return item.text return item.text
} }
/** /**
* Retrieves a formatted string resource using the provided ID and arguments. * Retrieves a formatted string using the specified string resource and arguments.
* *
* @param id The ID of the string resource to retrieve. * @param resource The string resource to be used.
* @param formatArgs The arguments to be inserted into the formatted string. * @param formatArgs The arguments to be inserted into the formatted string.
* @return The formatted string resource. * @return The formatted string resource.
* *
@ -87,67 +112,68 @@ private suspend fun loadString(id: ResourceId, resourceReader: ResourceReader):
*/ */
@ExperimentalResourceApi @ExperimentalResourceApi
@Composable @Composable
fun getString(id: ResourceId, vararg formatArgs: Any): String { fun getString(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(id, { "" }) { loadString(id, args, resourceReader) } val str by rememberState(resource, { "" }) { loadString(resource, args, resourceReader) }
return str return str
} }
/** /**
* Loads a formatted string resource using the provided ID and arguments. * Loads a formatted string using the specified string resource and arguments.
* *
* @param id The ID of the string resource to load. * @param resource The string resource to be used.
* @param formatArgs The arguments to be inserted into the formatted string. * @param formatArgs The arguments to be inserted into the formatted string.
* @return The formatted string resource. * @return The formatted string resource.
* *
* @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(id: ResourceId, vararg formatArgs: Any): String = loadString( suspend fun loadString(resource: StringResource, vararg formatArgs: Any): String = loadString(
id, resource,
formatArgs.map { it.toString() }, formatArgs.map { it.toString() },
DefaultResourceReader DefaultResourceReader
) )
private suspend fun loadString(id: ResourceId, args: List<String>, resourceReader: ResourceReader): String { private suspend fun loadString(resource: StringResource, args: List<String>, resourceReader: ResourceReader): String {
val str = loadString(id, resourceReader) val str = loadString(resource, resourceReader)
return SimpleStringFormatRegex.replace(str) { matchResult -> return SimpleStringFormatRegex.replace(str) { matchResult ->
args[matchResult.groupValues[1].toInt() - 1] args[matchResult.groupValues[1].toInt() - 1]
} }
} }
/** /**
* Retrieves a list of strings from a string array resource. * Retrieves a list of strings using the specified string array resource.
* *
* @param id The ID of the string array resource. * @param resource The string resource to be used.
* @return A list of strings representing the items in the string array. * @return A list of strings representing the items in the string array.
* *
* @throws IllegalStateException if the string array with the given ID is not found. * @throws IllegalStateException if the string array with the given ID is not found.
*/ */
@ExperimentalResourceApi @ExperimentalResourceApi
@Composable @Composable
fun getStringArray(id: ResourceId): List<String> { fun getStringArray(resource: StringResource): List<String> {
val resourceReader = LocalResourceReader.current val resourceReader = LocalResourceReader.current
val array by rememberState(id, { emptyList() }) { loadStringArray(id, resourceReader) } val array by rememberState(resource, { emptyList() }) { loadStringArray(resource, resourceReader) }
return array return array
} }
/** /**
* Loads a string array from a resource file. * Loads a list of strings using the specified string array resource.
* *
* @param id The ID of the string array resource. * @param resource The string resource to be used.
* @return A list of strings representing the items in the string array. * @return A list of strings representing the items in the string array.
* *
* @throws IllegalStateException if the string array with the given ID is not found. * @throws IllegalStateException if the string array with the given ID is not found.
*/ */
@ExperimentalResourceApi @ExperimentalResourceApi
suspend fun loadStringArray(id: ResourceId): List<String> = loadStringArray(id, DefaultResourceReader) suspend fun loadStringArray(resource: StringResource): List<String> = loadStringArray(resource, DefaultResourceReader)
private suspend fun loadStringArray(id: ResourceId, resourceReader: ResourceReader): List<String> { private suspend fun loadStringArray(resource: StringResource, resourceReader: ResourceReader): List<String> {
val nameToValue = getParsedStrings(getPathById(STRINGS_XML), resourceReader) val path = resource.getPathByEnvironment()
val item = nameToValue[id.stringKey] as? StringItem.Array val keyToValue = getParsedStrings(path, resourceReader)
?: error("String array ID=`${id.stringKey}` is not found!") val item = keyToValue[resource.key] as? StringItem.Array
?: error("String array ID=`${resource.key}` is not found!")
return item.items return item.items
} }

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

@ -14,12 +14,12 @@ import kotlin.test.assertNotEquals
class ResourceTest { class ResourceTest {
@Test @Test
fun testResourceEquals() = runBlockingTest { fun testResourceEquals() = runBlockingTest {
assertEquals(getPathById("a"), getPathById("a")) assertEquals(ImageResource("a"), ImageResource("a"))
} }
@Test @Test
fun testResourceNotEquals() = runBlockingTest { fun testResourceNotEquals() = runBlockingTest {
assertNotEquals(getPathById("a"), getPathById("b")) assertNotEquals(ImageResource("a"), ImageResource("b"))
} }
@Test @Test
@ -28,7 +28,7 @@ class ResourceTest {
readResourceBytes("missing.png") readResourceBytes("missing.png")
} }
val error = assertFailsWith<IllegalStateException> { val error = assertFailsWith<IllegalStateException> {
loadString("unknown_id") loadString(TestStringResource("unknown_id"))
} }
assertEquals("String ID=`unknown_id` is not found!", error.message) assertEquals("String ID=`unknown_id` is not found!", error.message)
} }
@ -56,11 +56,11 @@ class ResourceTest {
@Test @Test
fun testLoadStringResource() = runBlockingTest { fun testLoadStringResource() = runBlockingTest {
assertEquals("Compose Resources App", loadString("app_name")) assertEquals("Compose Resources App", loadString(TestStringResource("app_name")))
assertEquals( assertEquals(
"Hello, test-name! You have 42 new messages.", "Hello, test-name! You have 42 new messages.",
loadString("str_template", "test-name", 42) loadString(TestStringResource("str_template"), "test-name", 42)
) )
assertEquals(listOf("item 1", "item 2", "item 3"), loadStringArray("str_arr")) assertEquals(listOf("item 1", "item 2", "item 3"), loadStringArray(TestStringResource("str_arr")))
} }
} }

6
components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt

@ -3,3 +3,9 @@ package org.jetbrains.compose.resources
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
expect fun runBlockingTest(block: suspend CoroutineScope.() -> Unit) expect fun runBlockingTest(block: suspend CoroutineScope.() -> Unit)
internal fun TestStringResource(key: String) = StringResource(
"STRING:$key",
key,
setOf(ResourceItem(emptySet(), "strings.xml"))
)

38
components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.desktop.kt

@ -25,17 +25,17 @@ class ComposeResourceTest {
@Test @Test
fun testCountRecompositions() = runComposeUiTest { fun testCountRecompositions() = runComposeUiTest {
runBlockingTest { runBlockingTest {
val imagePathFlow = MutableStateFlow("1.png") val imagePathFlow = MutableStateFlow(ImageResource("1.png"))
val recompositionsCounter = RecompositionsCounter() val recompositionsCounter = RecompositionsCounter()
setContent { setContent {
val path by imagePathFlow.collectAsState() val res by imagePathFlow.collectAsState()
val res = imageResource(path) val imgRes = imageResource(res)
recompositionsCounter.content { recompositionsCounter.content {
Image(bitmap = res, contentDescription = null) Image(bitmap = imgRes, contentDescription = null)
} }
} }
awaitIdle() awaitIdle()
imagePathFlow.emit("2.png") imagePathFlow.emit(ImageResource("2.png"))
awaitIdle() awaitIdle()
assertEquals(2, recompositionsCounter.count) assertEquals(2, recompositionsCounter.count)
} }
@ -45,17 +45,17 @@ class ComposeResourceTest {
fun testImageResourceCache() = runComposeUiTest { fun testImageResourceCache() = runComposeUiTest {
runBlockingTest { runBlockingTest {
val testResourceReader = TestResourceReader() val testResourceReader = TestResourceReader()
val imagePathFlow = MutableStateFlow("1.png") val imagePathFlow = MutableStateFlow(ImageResource("1.png"))
setContent { setContent {
CompositionLocalProvider(LocalResourceReader provides testResourceReader) { CompositionLocalProvider(LocalResourceReader provides testResourceReader) {
val path by imagePathFlow.collectAsState() val res by imagePathFlow.collectAsState()
Image(painterResource(path), null) Image(painterResource(res), null)
} }
} }
awaitIdle() awaitIdle()
imagePathFlow.emit("2.png") imagePathFlow.emit(ImageResource("2.png"))
awaitIdle() awaitIdle()
imagePathFlow.emit("1.png") imagePathFlow.emit(ImageResource("1.png"))
awaitIdle() awaitIdle()
assertEquals( assertEquals(
@ -69,18 +69,18 @@ class ComposeResourceTest {
fun testStringResourceCache() = runComposeUiTest { fun testStringResourceCache() = runComposeUiTest {
runBlockingTest { runBlockingTest {
val testResourceReader = TestResourceReader() val testResourceReader = TestResourceReader()
val stringIdFlow = MutableStateFlow("app_name") val stringIdFlow = MutableStateFlow(TestStringResource("app_name"))
setContent { setContent {
CompositionLocalProvider(LocalResourceReader provides testResourceReader) { CompositionLocalProvider(LocalResourceReader provides testResourceReader) {
val textId by stringIdFlow.collectAsState() val res by stringIdFlow.collectAsState()
Text(getString(textId)) Text(getString(res))
Text(getStringArray("str_arr").joinToString()) Text(getStringArray(TestStringResource("str_arr")).joinToString())
} }
} }
awaitIdle() awaitIdle()
stringIdFlow.emit("hello") stringIdFlow.emit(TestStringResource("hello"))
awaitIdle() awaitIdle()
stringIdFlow.emit("app_name") stringIdFlow.emit(TestStringResource("app_name"))
awaitIdle() awaitIdle()
assertEquals( assertEquals(
@ -94,12 +94,12 @@ class ComposeResourceTest {
fun testReadStringResource() = runComposeUiTest { fun testReadStringResource() = runComposeUiTest {
runBlockingTest { runBlockingTest {
setContent { setContent {
assertEquals("Compose Resources App", getString("app_name")) assertEquals("Compose Resources App", getString(TestStringResource("app_name")))
assertEquals( assertEquals(
"Hello, test-name! You have 42 new messages.", "Hello, test-name! You have 42 new messages.",
getString("str_template", "test-name", 42) getString(TestStringResource("str_template"), "test-name", 42)
) )
assertEquals(listOf("item 1", "item 2", "item 3"), getStringArray("str_arr")) assertEquals(listOf("item 1", "item 2", "item 3"), getStringArray(TestStringResource("str_arr")))
} }
awaitIdle() awaitIdle()
} }

9
components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt

@ -33,11 +33,12 @@ private val defaultEmptyFont by lazy { Font("org.jetbrains.compose.emptyFont", B
@ExperimentalResourceApi @ExperimentalResourceApi
@Composable @Composable
actual fun Font(id: ResourceId, 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(id, { defaultEmptyFont }) { val fontFile by rememberState(resource, { defaultEmptyFont }) {
val fontBytes = resourceReader.read(getPathById(id)) val path = resource.getPathByEnvironment()
Font(id, fontBytes, weight, style) val fontBytes = resourceReader.read(path)
Font(path, fontBytes, weight, style)
} }
return fontFile return fontFile
} }

3
gradle-plugins/build.gradle.kts

@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
plugins { plugins {
alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.publish.plugin.portal) apply false alias(libs.plugins.publish.plugin) apply false
alias(libs.plugins.shadow.jar) apply false alias(libs.plugins.shadow.jar) apply false
alias(libs.plugins.download) apply false alias(libs.plugins.download) apply false
} }
@ -14,6 +14,7 @@ subprojects {
repositories { repositories {
mavenCentral() mavenCentral()
google()
mavenLocal() mavenLocal()
} }

11
gradle-plugins/compose/build.gradle.kts

@ -1,9 +1,10 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import de.undercouch.gradle.tasks.download.Download import de.undercouch.gradle.tasks.download.Download
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
plugins { plugins {
alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.publish.plugin.portal) alias(libs.plugins.publish.plugin)
id("java-gradle-plugin") id("java-gradle-plugin")
id("maven-publish") id("maven-publish")
alias(libs.plugins.shadow.jar) alias(libs.plugins.shadow.jar)
@ -30,8 +31,9 @@ val buildConfig = tasks.register("buildConfig", GenerateBuildConfig::class.java)
fieldsToGenerate.put("composeVersion", BuildProperties.composeVersion(project)) fieldsToGenerate.put("composeVersion", BuildProperties.composeVersion(project))
fieldsToGenerate.put("composeGradlePluginVersion", BuildProperties.deployVersion(project)) fieldsToGenerate.put("composeGradlePluginVersion", BuildProperties.deployVersion(project))
} }
tasks.named("compileKotlin") { tasks.named("compileKotlin", KotlinCompilationTask::class) {
dependsOn(buildConfig) dependsOn(buildConfig)
compilerOptions.freeCompilerArgs.add("-opt-in=org.jetbrains.compose.ExperimentalComposeLibrary")
} }
sourceSets.main.configure { sourceSets.main.configure {
java.srcDir(buildConfig.flatMap { it.generatedOutputDir }) java.srcDir(buildConfig.flatMap { it.generatedOutputDir })
@ -58,11 +60,14 @@ dependencies {
compileOnly(kotlin("gradle-plugin-api")) compileOnly(kotlin("gradle-plugin-api"))
compileOnly(kotlin("gradle-plugin")) compileOnly(kotlin("gradle-plugin"))
compileOnly(kotlin("native-utils")) compileOnly(kotlin("native-utils"))
compileOnly(libs.plugin.android)
compileOnly(libs.plugin.android.api)
testImplementation(gradleTestKit()) testImplementation(gradleTestKit())
testImplementation(kotlin("gradle-plugin-api")) testImplementation(kotlin("gradle-plugin-api"))
embedded(libs.download.task) embedded(libs.download.task)
embedded(libs.kotlin.poet)
embedded(project(":preview-rpc")) embedded(project(":preview-rpc"))
embedded(project(":jdk-version-probe")) embedded(project(":jdk-version-probe"))
} }
@ -98,7 +103,7 @@ val gradleTestsPattern = "org.jetbrains.compose.test.tests.integration.*"
tasks.registerVerificationTask<CheckJarPackagesTask>("checkJar") { tasks.registerVerificationTask<CheckJarPackagesTask>("checkJar") {
dependsOn(jar) dependsOn(jar)
jarFile.set(jar.archiveFile) jarFile.set(jar.archiveFile)
allowedPackagePrefixes.addAll("org.jetbrains.compose", "kotlinx.serialization") allowedPackagePrefixes.addAll("org.jetbrains.compose", "kotlinx.serialization", "com.squareup.kotlinpoet")
} }
tasks.test { tasks.test {

5
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt

@ -22,13 +22,14 @@ import org.jetbrains.compose.experimental.dsl.ExperimentalExtension
import org.jetbrains.compose.experimental.internal.configureExperimentalTargetsFlagsCheck import org.jetbrains.compose.experimental.internal.configureExperimentalTargetsFlagsCheck
import org.jetbrains.compose.experimental.internal.configureExperimental import org.jetbrains.compose.experimental.internal.configureExperimental
import org.jetbrains.compose.experimental.internal.configureNativeCompilerCaching import org.jetbrains.compose.experimental.internal.configureNativeCompilerCaching
import org.jetbrains.compose.experimental.uikit.internal.resources.configureSyncTask
import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID
import org.jetbrains.compose.internal.mppExt import org.jetbrains.compose.internal.mppExt
import org.jetbrains.compose.internal.mppExtOrNull import org.jetbrains.compose.internal.mppExtOrNull
import org.jetbrains.compose.internal.service.ConfigurationProblemReporterService import org.jetbrains.compose.internal.service.ConfigurationProblemReporterService
import org.jetbrains.compose.internal.service.GradlePropertySnapshotService import org.jetbrains.compose.internal.service.GradlePropertySnapshotService
import org.jetbrains.compose.internal.utils.currentTarget import org.jetbrains.compose.internal.utils.currentTarget
import org.jetbrains.compose.resources.configureResourceGenerator
import org.jetbrains.compose.resources.ios.configureSyncTask
import org.jetbrains.compose.web.WebExtension import org.jetbrains.compose.web.WebExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion
@ -64,6 +65,8 @@ abstract class ComposePlugin : Plugin<Project> {
project.plugins.apply(ComposeCompilerKotlinSupportPlugin::class.java) project.plugins.apply(ComposeCompilerKotlinSupportPlugin::class.java)
project.configureNativeCompilerCaching() project.configureNativeCompilerCaching()
project.configureResourceGenerator()
project.afterEvaluate { project.afterEvaluate {
configureDesktop(project, desktopExtension) configureDesktop(project, desktopExtension)
project.configureExperimental(composeExtension, experimentalExtension) project.configureExperimental(composeExtension, experimentalExtension)

8
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt

@ -21,6 +21,8 @@ internal object ComposeProperties {
internal const val MAC_NOTARIZATION_PASSWORD = "compose.desktop.mac.notarization.password" internal const val MAC_NOTARIZATION_PASSWORD = "compose.desktop.mac.notarization.password"
internal const val MAC_NOTARIZATION_TEAM_ID_PROVIDER = "compose.desktop.mac.notarization.teamID" internal const val MAC_NOTARIZATION_TEAM_ID_PROVIDER = "compose.desktop.mac.notarization.teamID"
internal const val CHECK_JDK_VENDOR = "compose.desktop.packaging.checkJdkVendor" internal const val CHECK_JDK_VENDOR = "compose.desktop.packaging.checkJdkVendor"
internal const val ALWAYS_GENERATE_RESOURCE_ACCESSORS = "compose.resources.always.generate.accessors"
internal const val SYNC_RESOURCES_PROPERTY = "compose.ios.resources.sync"
fun isVerbose(providers: ProviderFactory): Provider<Boolean> = fun isVerbose(providers: ProviderFactory): Provider<Boolean> =
providers.valueOrNull(VERBOSE).toBooleanProvider(false) providers.valueOrNull(VERBOSE).toBooleanProvider(false)
@ -51,4 +53,10 @@ internal object ComposeProperties {
fun checkJdkVendor(providers: ProviderFactory): Provider<Boolean> = fun checkJdkVendor(providers: ProviderFactory): Provider<Boolean> =
providers.valueOrNull(CHECK_JDK_VENDOR).toBooleanProvider(true) providers.valueOrNull(CHECK_JDK_VENDOR).toBooleanProvider(true)
fun alwaysGenerateResourceAccessors(providers: ProviderFactory): Provider<Boolean> =
providers.valueOrNull(ALWAYS_GENERATE_RESOURCE_ACCESSORS).toBooleanProvider(false)
fun syncResources(providers: ProviderFactory): Provider<Boolean> =
providers.valueOrNull(SYNC_RESOURCES_PROPERTY).toBooleanProvider(true)
} }

18
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/IosGradleProperties.kt

@ -1,18 +0,0 @@
/*
* Copyright 2020-2023 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.experimental.uikit.internal.utils
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ProviderFactory
import org.jetbrains.compose.internal.utils.valueOrNull
import org.jetbrains.compose.internal.utils.toBooleanProvider
internal object IosGradleProperties {
const val SYNC_RESOURCES_PROPERTY = "compose.ios.resources.sync"
fun syncResources(providers: ProviderFactory): Provider<Boolean> =
providers.valueOrNull(SYNC_RESOURCES_PROPERTY).toBooleanProvider(true)
}

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

@ -0,0 +1,42 @@
package org.jetbrains.compose.resources
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.tasks.MergeSourceSetFolders
import org.gradle.api.Project
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.SourceSet
import org.jetbrains.compose.internal.utils.registerTask
import java.io.File
internal fun Project.configureAndroidResources(
commonResourcesDir: Provider<File>,
androidFontsDir: Provider<File>,
onlyIfProvider: Provider<Boolean>
) {
val androidExtension = project.extensions.findByName("android") as? BaseExtension ?: return
val androidComponents = project.extensions.findByType(AndroidComponentsExtension::class.java) ?: return
val androidMainSourceSet = androidExtension.sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME)
androidMainSourceSet.resources.srcDir(commonResourcesDir)
androidMainSourceSet.assets.srcDir(androidFontsDir)
val copyFonts = registerTask<Copy>("copyFontsToAndroidAssets") {
includeEmptyDirs = false
from(commonResourcesDir)
include("**/fonts/*")
into(androidFontsDir)
onlyIf { onlyIfProvider.get() }
}
androidComponents.onVariants { variant ->
variant.sources?.assets?.addGeneratedSourceDirectory(
taskProvider = copyFonts,
wiredWith = {
objects.directoryProperty().fileProvider(
copyFonts.map { t -> t.destinationDir }
)
}
)
}
}

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

@ -0,0 +1,98 @@
package org.jetbrains.compose.resources
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import java.io.File
import java.nio.file.Path
import javax.xml.parsers.DocumentBuilderFactory
import kotlin.io.path.relativeTo
/**
* This task should be FAST and SAFE! Because it is being run during IDE import.
*/
abstract class GenerateResClassTask : DefaultTask() {
@get:Input
abstract val packageName: Property<String>
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val resDir: DirectoryProperty
@get:OutputDirectory
abstract val codeDir: DirectoryProperty
init {
this.onlyIf { resDir.asFile.get().exists() }
}
@TaskAction
fun generate() {
try {
val rootResDir = resDir.get().asFile
logger.info("Generate resources for $rootResDir")
//get first level dirs
val dirs = rootResDir.listFiles { f -> f.isDirectory }.orEmpty()
//type -> id -> resource item
val resources: Map<ResourceType, Map<String, List<ResourceItem>>> = dirs
.flatMap { dir ->
dir.listFiles { f -> !f.isDirectory }
.orEmpty()
.mapNotNull { it.fileToResourceItems(rootResDir.parentFile.toPath()) }
.flatten()
}
.groupBy { it.type }
.mapValues { (_, items) -> items.groupBy { it.name } }
val kotlinDir = codeDir.get().asFile
kotlinDir.deleteRecursively()
kotlinDir.mkdirs()
getResFileSpec(resources, packageName.get()).writeTo(kotlinDir)
} catch (e: Exception) {
//message must contain two ':' symbols to be parsed by IDE UI!
logger.error("e: GenerateResClassTask was failed:", e)
}
}
private fun File.fileToResourceItems(
relativeTo: Path
): List<ResourceItem>? {
val file = this
if (file.isDirectory) return null
val dirName = file.parentFile.name ?: return null
val typeAndQualifiers = dirName.lowercase().split("-")
if (typeAndQualifiers.isEmpty()) return null
val typeString = typeAndQualifiers.first().lowercase()
val qualifiers = typeAndQualifiers.takeLast(typeAndQualifiers.size - 1).map { it.lowercase() }.toSet()
val path = file.toPath().relativeTo(relativeTo)
return if (typeString == "values" && file.name.equals("strings.xml", true)) {
val stringIds = getStringIds(file)
stringIds.map { strId ->
ResourceItem(ResourceType.STRING, qualifiers, strId.lowercase(), path)
}
} else {
val type = try {
ResourceType.fromString(typeString)
} catch (e: Exception) {
logger.error("e: Error: $path", e)
return null
}
listOf(ResourceItem(type, qualifiers, file.nameWithoutExtension.lowercase(), path))
}
}
private val stringTypeNames = listOf("string", "string-array")
private fun getStringIds(stringsXml: File): Set<String> {
val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(stringsXml)
val items = doc.getElementsByTagName("resources").item(0).childNodes
val ids = List(items.length) { items.item(it) }
.filter { it.nodeName in stringTypeNames }
.map { it.attributes.getNamedItem("name").nodeValue }
return ids.toSet()
}
}

77
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesGenerator.kt

@ -0,0 +1,77 @@
package org.jetbrains.compose.resources
import org.gradle.api.Project
import org.gradle.api.provider.Provider
import org.jetbrains.compose.ComposeExtension
import org.jetbrains.compose.ComposePlugin
import org.jetbrains.compose.ExperimentalComposeLibrary
import org.jetbrains.compose.desktop.application.internal.ComposeProperties
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import java.io.File
private const val COMPOSE_RESOURCES_DIR = "composeRes"
private const val RES_GEN_DIR = "generated/compose/resourceGenerator"
internal fun Project.configureResourceGenerator() {
val kotlinExtension = project.extensions.getByType(KotlinProjectExtension::class.java)
val commonSourceSet = kotlinExtension.sourceSets.findByName(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME) ?: return
val commonResourcesDir = provider { commonSourceSet.resources.sourceDirectories.first() }
val packageName = provider {
buildString {
val group = project.group.toString()
append(group)
if (group.isNotEmpty()) append(".")
append("generated.resources")
}
}
fun buildDir(path: String) = layout.dir(layout.buildDirectory.map { File(it.asFile, path) })
val resDir = layout.dir(commonResourcesDir.map { it.resolve(COMPOSE_RESOURCES_DIR) })
//lazy check a dependency on the Resources library
val shouldGenerateResourceAccessors: Provider<Boolean> = provider {
if (ComposeProperties.alwaysGenerateResourceAccessors(providers).get()) {
true
} else {
configurations
.getByName(commonSourceSet.implementationConfigurationName)
.allDependencies.any { dep ->
val depStringNotation = dep.let { "${it.group}:${it.name}:${it.version}" }
depStringNotation == ComposePlugin.CommonComponentsDependencies.resources
}
}
}
val genTask = tasks.register(
"generateComposeResClass",
GenerateResClassTask::class.java
) {
it.packageName.set(packageName)
it.resDir.set(resDir)
it.codeDir.set(buildDir("$RES_GEN_DIR/kotlin"))
it.onlyIf { shouldGenerateResourceAccessors.get() }
}
//register generated source set
commonSourceSet.kotlin.srcDir(genTask.map { it.codeDir })
//setup task execution during IDE import
tasks.configureEach {
if (it.name == "prepareKotlinIdeaImport") {
it.dependsOn(genTask)
}
}
val androidExtension = project.extensions.findByName("android")
if (androidExtension != null) {
configureAndroidResources(
commonResourcesDir,
buildDir("$RES_GEN_DIR/androidFonts").map { it.asFile },
shouldGenerateResourceAccessors
)
}
}

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

@ -0,0 +1,95 @@
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 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");
companion object {
fun fromString(str: String) = when (str) {
"images" -> ResourceType.IMAGE
"strings" -> ResourceType.STRING
"fonts" -> ResourceType.FONT
else -> error("Unknown resource type: $str")
}
}
}
internal data class ResourceItem(
val type: ResourceType,
val qualifiers: Set<String>,
val name: String,
val path: Path
)
private fun ResourceItem.getClassName(): ClassName = when (type) {
ResourceType.IMAGE -> ClassName("org.jetbrains.compose.resources", "ImageResource")
ResourceType.STRING -> ClassName("org.jetbrains.compose.resources", "StringResource")
ResourceType.FONT -> ClassName("org.jetbrains.compose.resources", "FontResource")
}
internal fun getResFileSpec(
//type -> id -> items
resources: Map<ResourceType, Map<String, List<ResourceItem>>>,
packageName: String
): FileSpec = FileSpec.builder(packageName, "Res").apply {
addType(TypeSpec.objectBuilder("Res").apply {
addModifiers(KModifier.INTERNAL)
val types = resources.map { (type, idToResources) ->
getResourceTypeObject(type, idToResources)
}.sortedBy { it.name }
addTypes(types)
}.build())
}.build()
private fun getResourceTypeObject(type: ResourceType, nameToResources: Map<String, List<ResourceItem>>) =
TypeSpec.objectBuilder(type.typeName).apply {
nameToResources.entries
.sortedBy { it.key }
.forEach { (name, items) ->
addResourceProperty(name, items.sortedBy { it.path })
}
}.build()
private fun TypeSpec.Builder.addResourceProperty(name: String, items: List<ResourceItem>) {
val resourceItemClass = ClassName("org.jetbrains.compose.resources", "ResourceItem")
val first = items.first()
val propertyClassName = first.getClassName()
val resourceId = first.let { "${it.type}:${it.name}" }
val initializer = CodeBlock.builder()
.add("%T(\n", propertyClassName).withIndent {
add("\"$resourceId\",\n")
if (first.type == ResourceType.STRING) {
add("\"${first.name}\",\n")
}
add("setOf(\n").withIndent {
items.forEach { item ->
val qualifiers = item.qualifiers.sorted().joinToString { "\"$it\"" }
//file separator should be '/' on all platforms
add("%T(setOf($qualifiers), \"${item.path.invariantSeparatorsPathString}\"),\n", resourceItemClass)
}
}
add(")\n")
}
.add(")")
.build()
addProperty(
PropertySpec.builder(name, propertyClassName)
.initializer(initializer)
.build()
)
}

2
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/IosTargetResources.kt → gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/IosTargetResources.kt

@ -3,7 +3,7 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/ */
package org.jetbrains.compose.experimental.uikit.internal.resources package org.jetbrains.compose.resources.ios
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
import org.gradle.api.provider.SetProperty import org.gradle.api.provider.SetProperty

5
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/tasks/SyncComposeResourcesForIosTask.kt → gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/SyncComposeResourcesForIosTask.kt

@ -3,15 +3,14 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/ */
package org.jetbrains.compose.experimental.uikit.tasks package org.jetbrains.compose.resources.ios
import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileCollection import org.gradle.api.file.FileCollection
import org.gradle.api.provider.Provider import org.gradle.api.provider.Provider
import org.gradle.api.provider.SetProperty import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.* import org.gradle.api.tasks.*
import org.jetbrains.compose.experimental.uikit.internal.resources.determineIosKonanTargetsFromEnv import org.jetbrains.compose.experimental.uikit.tasks.AbstractComposeIosTask
import org.jetbrains.compose.experimental.uikit.internal.resources.IosTargetResources
import org.jetbrains.compose.internal.utils.clearDirs import org.jetbrains.compose.internal.utils.clearDirs
import java.io.File import java.io.File
import kotlin.io.path.Path import kotlin.io.path.Path

11
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/configureSyncIosResources.kt → gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/configureSyncIosResources.kt

@ -3,7 +3,7 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/ */
package org.jetbrains.compose.experimental.uikit.internal.resources package org.jetbrains.compose.resources.ios
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.file.Directory import org.gradle.api.file.Directory
@ -11,12 +11,11 @@ import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Input import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskAction
import org.jetbrains.compose.experimental.uikit.internal.utils.IosGradleProperties import org.jetbrains.compose.desktop.application.internal.ComposeProperties
import org.jetbrains.compose.experimental.uikit.internal.utils.asIosNativeTargetOrNull import org.jetbrains.compose.experimental.uikit.internal.utils.asIosNativeTargetOrNull
import org.jetbrains.compose.experimental.uikit.internal.utils.cocoapodsExt import org.jetbrains.compose.experimental.uikit.internal.utils.cocoapodsExt
import org.jetbrains.compose.experimental.uikit.internal.utils.withCocoapodsPlugin import org.jetbrains.compose.experimental.uikit.internal.utils.withCocoapodsPlugin
import org.jetbrains.compose.experimental.uikit.tasks.AbstractComposeIosTask import org.jetbrains.compose.experimental.uikit.tasks.AbstractComposeIosTask
import org.jetbrains.compose.experimental.uikit.tasks.SyncComposeResourcesForIosTask
import org.jetbrains.compose.internal.utils.joinLowerCamelCase import org.jetbrains.compose.internal.utils.joinLowerCamelCase
import org.jetbrains.compose.internal.utils.new import org.jetbrains.compose.internal.utils.new
import org.jetbrains.compose.internal.utils.registerOrConfigure import org.jetbrains.compose.internal.utils.registerOrConfigure
@ -36,8 +35,8 @@ internal fun Project.configureSyncTask(mppExt: KotlinMultiplatformExtension) {
logger.info("Compose Multiplatform resource management for iOS is disabled: $reason") logger.info("Compose Multiplatform resource management for iOS is disabled: $reason")
} }
if (!IosGradleProperties.syncResources(providers).get()) { if (!ComposeProperties.syncResources(providers).get()) {
reportSyncIsDisabled("'${IosGradleProperties.SYNC_RESOURCES_PROPERTY}' value is 'false'") reportSyncIsDisabled("'${ComposeProperties.SYNC_RESOURCES_PROPERTY}' value is 'false'")
return return
} }
@ -98,7 +97,7 @@ private fun SyncIosResourcesContext.configureCocoapodsResourcesAttribute() {
error(""" error("""
|Kotlin.cocoapods.extraSpecAttributes["resources"] is not compatible with Compose Multiplatform's resources management for iOS. |Kotlin.cocoapods.extraSpecAttributes["resources"] is not compatible with Compose Multiplatform's resources management for iOS.
| * Recommended action: remove extraSpecAttributes["resources"] from '${project.buildFile}' and run '${project.path}:podInstall' once; | * Recommended action: remove extraSpecAttributes["resources"] from '${project.buildFile}' and run '${project.path}:podInstall' once;
| * Alternative action: turn off Compose Multiplatform's resources management for iOS by adding '${IosGradleProperties.SYNC_RESOURCES_PROPERTY}=false' to your gradle.properties; | * Alternative action: turn off Compose Multiplatform's resources management for iOS by adding '${ComposeProperties.SYNC_RESOURCES_PROPERTY}=false' to your gradle.properties;
""".trimMargin()) """.trimMargin())
} }
cocoapodsExt.framework { cocoapodsExt.framework {

2
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/determineIosKonanTargetsFromEnv.kt → gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/determineIosKonanTargetsFromEnv.kt

@ -3,7 +3,7 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/ */
package org.jetbrains.compose.experimental.uikit.internal.resources package org.jetbrains.compose.resources.ios
import org.jetbrains.kotlin.konan.target.KonanTarget import org.jetbrains.kotlin.konan.target.KonanTarget

46
gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt

@ -0,0 +1,46 @@
package org.jetbrains.compose.test.tests.integration
import org.jetbrains.compose.test.utils.GradlePluginTestBase
import org.jetbrains.compose.test.utils.assertEqualTextFiles
import org.jetbrains.compose.test.utils.assertNotEqualTextFiles
import org.jetbrains.compose.test.utils.checks
import org.junit.jupiter.api.Test
import kotlin.io.path.Path
class ResourcesTest : GradlePluginTestBase() {
@Test
fun testGeneratedAccessorsAndCopiedFonts() = with(testProject("misc/commonResources")) {
//check generated resource's accessors
gradle("generateComposeResClass").checks {
assertEqualTextFiles(
file("build/generated/compose/resourceGenerator/kotlin/generated/resources/Res.kt"),
file("expected/Res.kt")
)
check.logContains("""
java.lang.IllegalStateException: Unknown resource type: ignored
""".trimIndent())
}
file("src/commonMain/resources/composeRes/images/vector_2.xml").renameTo(
file("src/commonMain/resources/composeRes/images/vector_3.xml")
)
//check resource's accessors were regenerated
gradle("generateComposeResClass").checks {
assertNotEqualTextFiles(
file("build/generated/compose/resourceGenerator/kotlin/generated/resources/Res.kt"),
file("expected/Res.kt")
)
}
file("src/commonMain/resources/composeRes/images/vector_3.xml").renameTo(
file("src/commonMain/resources/composeRes/images/vector_2.xml")
)
//TODO: check a real build after a release a new version of the resources library
//because generated accessors depend on classes from the new version
gradle("assembleDebug", "--dry-run").checks {
check.taskSkipped("copyFontsToAndroidAssets")
}
}
}

2
gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProject.kt

@ -15,6 +15,7 @@ import java.util.Properties
data class TestEnvironment( data class TestEnvironment(
val workingDir: File, val workingDir: File,
val kotlinVersion: String = TestKotlinVersions.Default, val kotlinVersion: String = TestKotlinVersions.Default,
val agpVersion: String = "7.3.1",
val composeGradlePluginVersion: String = TestProperties.composeGradlePluginVersion, val composeGradlePluginVersion: String = TestProperties.composeGradlePluginVersion,
val mokoResourcesPluginVersion: String = "0.23.0", val mokoResourcesPluginVersion: String = "0.23.0",
val composeCompilerPlugin: String? = null, val composeCompilerPlugin: String? = null,
@ -26,6 +27,7 @@ data class TestEnvironment(
private val placeholders = linkedMapOf( private val placeholders = linkedMapOf(
"COMPOSE_GRADLE_PLUGIN_VERSION_PLACEHOLDER" to composeGradlePluginVersion, "COMPOSE_GRADLE_PLUGIN_VERSION_PLACEHOLDER" to composeGradlePluginVersion,
"KOTLIN_VERSION_PLACEHOLDER" to kotlinVersion, "KOTLIN_VERSION_PLACEHOLDER" to kotlinVersion,
"AGP_VERSION_PLACEHOLDER" to agpVersion,
"COMPOSE_COMPILER_PLUGIN_PLACEHOLDER" to composeCompilerPlugin, "COMPOSE_COMPILER_PLUGIN_PLACEHOLDER" to composeCompilerPlugin,
"COMPOSE_COMPILER_PLUGIN_ARGS_PLACEHOLDER" to composeCompilerArgs, "COMPOSE_COMPILER_PLUGIN_ARGS_PLACEHOLDER" to composeCompilerArgs,
"MOKO_RESOURCES_PLUGIN_VERSION_PLACEHOLDER" to mokoResourcesPluginVersion, "MOKO_RESOURCES_PLUGIN_VERSION_PLACEHOLDER" to mokoResourcesPluginVersion,

2
gradle-plugins/compose/src/test/test-projects/application/mpp/settings.gradle

@ -2,7 +2,7 @@ pluginManagement {
plugins { plugins {
id 'org.jetbrains.kotlin.multiplatform' version 'KOTLIN_VERSION_PLACEHOLDER' id 'org.jetbrains.kotlin.multiplatform' version 'KOTLIN_VERSION_PLACEHOLDER'
id 'org.jetbrains.compose' version 'COMPOSE_GRADLE_PLUGIN_VERSION_PLACEHOLDER' id 'org.jetbrains.compose' version 'COMPOSE_GRADLE_PLUGIN_VERSION_PLACEHOLDER'
id 'com.android.application' version '7.0.4' id 'com.android.application' version 'AGP_VERSION_PLACEHOLDER'
} }
repositories { repositories {
mavenLocal() mavenLocal()

39
gradle-plugins/compose/src/test/test-projects/misc/commonResources/build.gradle.kts

@ -0,0 +1,39 @@
plugins {
kotlin("multiplatform")
id("com.android.library")
id("org.jetbrains.compose")
}
kotlin {
androidTarget {
compilations.all {
kotlinOptions {
jvmTarget = "11"
}
}
}
jvm("desktop")
sourceSets {
commonMain {
dependencies {
implementation(compose.runtime)
implementation(compose.material)
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
implementation(compose.components.resources)
}
}
}
}
android {
compileSdk = 31
namespace = "org.jetbrains.compose.resources.test"
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}

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

@ -0,0 +1,78 @@
package generated.resources
import org.jetbrains.compose.resources.FontResource
import org.jetbrains.compose.resources.ImageResource
import org.jetbrains.compose.resources.ResourceItem
import org.jetbrains.compose.resources.StringResource
internal object Res {
public object fonts {
public val emptyfont: FontResource = FontResource(
"FONT:emptyfont",
setOf(
ResourceItem(setOf(), "composeRes/fonts/emptyFont.otf"),
)
)
}
public object images {
public val vector: ImageResource = ImageResource(
"IMAGE:vector",
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"),
)
)
public val vector_2: ImageResource = ImageResource(
"IMAGE:vector_2",
setOf(
ResourceItem(setOf(), "composeRes/images/vector_2.xml"),
)
)
}
public object strings {
public val app_name: StringResource = StringResource(
"STRING:app_name",
"app_name",
setOf(
ResourceItem(setOf(), "composeRes/values/strings.xml"),
)
)
public val hello: StringResource = StringResource(
"STRING:hello",
"hello",
setOf(
ResourceItem(setOf(), "composeRes/values/strings.xml"),
)
)
public val multi_line: StringResource = StringResource(
"STRING:multi_line",
"multi_line",
setOf(
ResourceItem(setOf(), "composeRes/values/strings.xml"),
)
)
public val str_arr: StringResource = StringResource(
"STRING:str_arr",
"str_arr",
setOf(
ResourceItem(setOf(), "composeRes/values/strings.xml"),
)
)
public val str_template: StringResource = StringResource(
"STRING:str_template",
"str_template",
setOf(
ResourceItem(setOf(), "composeRes/values/strings.xml"),
)
)
}
}

1
gradle-plugins/compose/src/test/test-projects/misc/commonResources/gradle.properties

@ -0,0 +1 @@
android.useAndroidX=true

22
gradle-plugins/compose/src/test/test-projects/misc/commonResources/settings.gradle.kts

@ -0,0 +1,22 @@
pluginManagement {
repositories {
mavenLocal()
gradlePluginPortal()
google()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
plugins {
id("com.android.library").version("AGP_VERSION_PLACEHOLDER")
id("org.jetbrains.kotlin.multiplatform").version("KOTLIN_VERSION_PLACEHOLDER")
id("org.jetbrains.compose").version("COMPOSE_GRADLE_PLUGIN_VERSION_PLACEHOLDER")
}
}
dependencyResolutionManagement {
repositories {
mavenLocal()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
mavenCentral()
gradlePluginPortal()
google()
}
}

20
gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/kotlin/App.kt

@ -0,0 +1,20 @@
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.font.FontFamily
import generated.resources.Res
import org.jetbrains.compose.resources.*
@OptIn(ExperimentalResourceApi::class)
@Composable
fun App() {
Column {
Image(
modifier = Modifier.size(100.dp),
painter = painterResource(Res.images.vector),
contentDescription = null
)
Text(getString(Res.strings.app_name))
val font = FontFamily(Font(Res.fonts.emptyfont))
}
}

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

Binary file not shown.

0
gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/ignored/ignored_3.txt

0
gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/ignored_1.txt

36
gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q1-q2/vector.xml

@ -0,0 +1,36 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="600dp"
android:height="600dp"
android:viewportWidth="600"
android:viewportHeight="600">
<path
android:pathData="M301.21,418.53C300.97,418.54 300.73,418.56 300.49,418.56C297.09,418.59 293.74,417.72 290.79,416.05L222.6,377.54C220.63,376.43 219,374.82 217.85,372.88C216.7,370.94 216.09,368.73 216.07,366.47L216.07,288.16C216.06,287.32 216.09,286.49 216.17,285.67C216.38,283.54 216.91,281.5 217.71,279.6L199.29,268.27L177.74,256.19C175.72,260.43 174.73,265.23 174.78,270.22L174.79,387.05C174.85,393.89 178.57,400.2 184.53,403.56L286.26,461.02C290.67,463.51 295.66,464.8 300.73,464.76C300.91,464.76 301.09,464.74 301.27,464.74C301.24,449.84 301.22,439.23 301.22,439.23L301.21,418.53Z"
android:fillColor="#041619"
android:fillType="nonZero"/>
<path
android:pathData="M409.45,242.91L312.64,188.23C303.64,183.15 292.58,183.26 283.68,188.51L187.92,245C183.31,247.73 179.93,251.62 177.75,256.17L177.74,256.19L199.29,268.27L217.71,279.6C217.83,279.32 217.92,279.02 218.05,278.74C218.24,278.36 218.43,277.98 218.64,277.62C219.06,276.88 219.52,276.18 220.04,275.51C221.37,273.8 223.01,272.35 224.87,271.25L289.06,233.39C290.42,232.59 291.87,231.96 293.39,231.51C295.53,230.87 297.77,230.6 300,230.72C302.98,230.88 305.88,231.73 308.47,233.2L373.37,269.85C375.54,271.08 377.49,272.68 379.13,274.57C379.68,275.19 380.18,275.85 380.65,276.53C380.86,276.84 381.05,277.15 381.24,277.47L397.79,266.39L420.34,252.93L420.31,252.88C417.55,248.8 413.77,245.35 409.45,242.91Z"
android:fillColor="#37BF6E"
android:fillType="nonZero"/>
<path
android:pathData="M381.24,277.47C381.51,277.92 381.77,278.38 382.01,278.84C382.21,279.24 382.39,279.65 382.57,280.06C382.91,280.88 383.19,281.73 383.41,282.59C383.74,283.88 383.92,285.21 383.93,286.57L383.93,361.1C383.96,363.95 383.35,366.77 382.16,369.36C381.93,369.86 381.69,370.35 381.42,370.83C379.75,373.79 377.32,376.27 374.39,378L310.2,415.87C307.47,417.48 304.38,418.39 301.21,418.53L301.22,439.23C301.22,439.23 301.24,449.84 301.27,464.74C306.1,464.61 310.91,463.3 315.21,460.75L410.98,404.25C419.88,399 425.31,389.37 425.22,379.03L425.22,267.85C425.17,262.48 423.34,257.34 420.34,252.93L397.79,266.39L381.24,277.47Z"
android:fillColor="#3870B2"
android:fillType="nonZero"/>
<path
android:pathData="M177.75,256.17C179.93,251.62 183.31,247.73 187.92,245L283.68,188.51C292.58,183.26 303.64,183.15 312.64,188.23L409.45,242.91C413.77,245.35 417.55,248.8 420.31,252.88L420.34,252.93L498.59,206.19C494.03,199.46 487.79,193.78 480.67,189.75L320.86,99.49C306.01,91.1 287.75,91.27 273.07,99.95L114.99,193.2C107.39,197.69 101.81,204.11 98.21,211.63L177.74,256.19L177.75,256.17ZM301.27,464.74C301.09,464.74 300.91,464.76 300.73,464.76C295.66,464.8 290.67,463.51 286.26,461.02L184.53,403.56C178.57,400.2 174.85,393.89 174.79,387.05L174.78,270.22C174.73,265.23 175.72,260.43 177.74,256.19L98.21,211.63C94.86,218.63 93.23,226.58 93.31,234.82L93.31,427.67C93.42,438.97 99.54,449.37 109.4,454.92L277.31,549.77C284.6,553.88 292.84,556.01 301.2,555.94L301.2,555.8C301.39,543.78 301.33,495.26 301.27,464.74Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
<path
android:pathData="M498.59,206.19L420.34,252.93C423.34,257.34 425.17,262.48 425.22,267.85L425.22,379.03C425.31,389.37 419.88,399 410.98,404.25L315.21,460.75C310.91,463.3 306.1,464.61 301.27,464.74C301.33,495.26 301.39,543.78 301.2,555.8L301.2,555.94C309.48,555.87 317.74,553.68 325.11,549.32L483.18,456.06C497.87,447.39 506.85,431.49 506.69,414.43L506.69,230.91C506.6,222.02 503.57,213.5 498.59,206.19Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
<path
android:pathData="M301.2,555.94C292.84,556.01 284.6,553.88 277.31,549.76L109.4,454.92C99.54,449.37 93.42,438.97 93.31,427.67L93.31,234.82C93.23,226.58 94.86,218.63 98.21,211.63C101.81,204.11 107.39,197.69 114.99,193.2L273.07,99.95C287.75,91.27 306.01,91.1 320.86,99.49L480.67,189.75C487.79,193.78 494.03,199.46 498.59,206.19C503.57,213.5 506.6,222.02 506.69,230.91L506.69,414.43C506.85,431.49 497.87,447.39 483.18,456.06L325.11,549.32C317.74,553.68 309.48,555.87 301.2,555.94Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
</vector>

36
gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q1/vector.xml

@ -0,0 +1,36 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="600dp"
android:height="600dp"
android:viewportWidth="600"
android:viewportHeight="600">
<path
android:pathData="M301.21,418.53C300.97,418.54 300.73,418.56 300.49,418.56C297.09,418.59 293.74,417.72 290.79,416.05L222.6,377.54C220.63,376.43 219,374.82 217.85,372.88C216.7,370.94 216.09,368.73 216.07,366.47L216.07,288.16C216.06,287.32 216.09,286.49 216.17,285.67C216.38,283.54 216.91,281.5 217.71,279.6L199.29,268.27L177.74,256.19C175.72,260.43 174.73,265.23 174.78,270.22L174.79,387.05C174.85,393.89 178.57,400.2 184.53,403.56L286.26,461.02C290.67,463.51 295.66,464.8 300.73,464.76C300.91,464.76 301.09,464.74 301.27,464.74C301.24,449.84 301.22,439.23 301.22,439.23L301.21,418.53Z"
android:fillColor="#041619"
android:fillType="nonZero"/>
<path
android:pathData="M409.45,242.91L312.64,188.23C303.64,183.15 292.58,183.26 283.68,188.51L187.92,245C183.31,247.73 179.93,251.62 177.75,256.17L177.74,256.19L199.29,268.27L217.71,279.6C217.83,279.32 217.92,279.02 218.05,278.74C218.24,278.36 218.43,277.98 218.64,277.62C219.06,276.88 219.52,276.18 220.04,275.51C221.37,273.8 223.01,272.35 224.87,271.25L289.06,233.39C290.42,232.59 291.87,231.96 293.39,231.51C295.53,230.87 297.77,230.6 300,230.72C302.98,230.88 305.88,231.73 308.47,233.2L373.37,269.85C375.54,271.08 377.49,272.68 379.13,274.57C379.68,275.19 380.18,275.85 380.65,276.53C380.86,276.84 381.05,277.15 381.24,277.47L397.79,266.39L420.34,252.93L420.31,252.88C417.55,248.8 413.77,245.35 409.45,242.91Z"
android:fillColor="#37BF6E"
android:fillType="nonZero"/>
<path
android:pathData="M381.24,277.47C381.51,277.92 381.77,278.38 382.01,278.84C382.21,279.24 382.39,279.65 382.57,280.06C382.91,280.88 383.19,281.73 383.41,282.59C383.74,283.88 383.92,285.21 383.93,286.57L383.93,361.1C383.96,363.95 383.35,366.77 382.16,369.36C381.93,369.86 381.69,370.35 381.42,370.83C379.75,373.79 377.32,376.27 374.39,378L310.2,415.87C307.47,417.48 304.38,418.39 301.21,418.53L301.22,439.23C301.22,439.23 301.24,449.84 301.27,464.74C306.1,464.61 310.91,463.3 315.21,460.75L410.98,404.25C419.88,399 425.31,389.37 425.22,379.03L425.22,267.85C425.17,262.48 423.34,257.34 420.34,252.93L397.79,266.39L381.24,277.47Z"
android:fillColor="#3870B2"
android:fillType="nonZero"/>
<path
android:pathData="M177.75,256.17C179.93,251.62 183.31,247.73 187.92,245L283.68,188.51C292.58,183.26 303.64,183.15 312.64,188.23L409.45,242.91C413.77,245.35 417.55,248.8 420.31,252.88L420.34,252.93L498.59,206.19C494.03,199.46 487.79,193.78 480.67,189.75L320.86,99.49C306.01,91.1 287.75,91.27 273.07,99.95L114.99,193.2C107.39,197.69 101.81,204.11 98.21,211.63L177.74,256.19L177.75,256.17ZM301.27,464.74C301.09,464.74 300.91,464.76 300.73,464.76C295.66,464.8 290.67,463.51 286.26,461.02L184.53,403.56C178.57,400.2 174.85,393.89 174.79,387.05L174.78,270.22C174.73,265.23 175.72,260.43 177.74,256.19L98.21,211.63C94.86,218.63 93.23,226.58 93.31,234.82L93.31,427.67C93.42,438.97 99.54,449.37 109.4,454.92L277.31,549.77C284.6,553.88 292.84,556.01 301.2,555.94L301.2,555.8C301.39,543.78 301.33,495.26 301.27,464.74Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
<path
android:pathData="M498.59,206.19L420.34,252.93C423.34,257.34 425.17,262.48 425.22,267.85L425.22,379.03C425.31,389.37 419.88,399 410.98,404.25L315.21,460.75C310.91,463.3 306.1,464.61 301.27,464.74C301.33,495.26 301.39,543.78 301.2,555.8L301.2,555.94C309.48,555.87 317.74,553.68 325.11,549.32L483.18,456.06C497.87,447.39 506.85,431.49 506.69,414.43L506.69,230.91C506.6,222.02 503.57,213.5 498.59,206.19Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
<path
android:pathData="M301.2,555.94C292.84,556.01 284.6,553.88 277.31,549.76L109.4,454.92C99.54,449.37 93.42,438.97 93.31,427.67L93.31,234.82C93.23,226.58 94.86,218.63 98.21,211.63C101.81,204.11 107.39,197.69 114.99,193.2L273.07,99.95C287.75,91.27 306.01,91.1 320.86,99.49L480.67,189.75C487.79,193.78 494.03,199.46 498.59,206.19C503.57,213.5 506.6,222.02 506.69,230.91L506.69,414.43C506.85,431.49 497.87,447.39 483.18,456.06L325.11,549.32C317.74,553.68 309.48,555.87 301.2,555.94Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
</vector>

36
gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q2/vector.xml

@ -0,0 +1,36 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="600dp"
android:height="600dp"
android:viewportWidth="600"
android:viewportHeight="600">
<path
android:pathData="M301.21,418.53C300.97,418.54 300.73,418.56 300.49,418.56C297.09,418.59 293.74,417.72 290.79,416.05L222.6,377.54C220.63,376.43 219,374.82 217.85,372.88C216.7,370.94 216.09,368.73 216.07,366.47L216.07,288.16C216.06,287.32 216.09,286.49 216.17,285.67C216.38,283.54 216.91,281.5 217.71,279.6L199.29,268.27L177.74,256.19C175.72,260.43 174.73,265.23 174.78,270.22L174.79,387.05C174.85,393.89 178.57,400.2 184.53,403.56L286.26,461.02C290.67,463.51 295.66,464.8 300.73,464.76C300.91,464.76 301.09,464.74 301.27,464.74C301.24,449.84 301.22,439.23 301.22,439.23L301.21,418.53Z"
android:fillColor="#041619"
android:fillType="nonZero"/>
<path
android:pathData="M409.45,242.91L312.64,188.23C303.64,183.15 292.58,183.26 283.68,188.51L187.92,245C183.31,247.73 179.93,251.62 177.75,256.17L177.74,256.19L199.29,268.27L217.71,279.6C217.83,279.32 217.92,279.02 218.05,278.74C218.24,278.36 218.43,277.98 218.64,277.62C219.06,276.88 219.52,276.18 220.04,275.51C221.37,273.8 223.01,272.35 224.87,271.25L289.06,233.39C290.42,232.59 291.87,231.96 293.39,231.51C295.53,230.87 297.77,230.6 300,230.72C302.98,230.88 305.88,231.73 308.47,233.2L373.37,269.85C375.54,271.08 377.49,272.68 379.13,274.57C379.68,275.19 380.18,275.85 380.65,276.53C380.86,276.84 381.05,277.15 381.24,277.47L397.79,266.39L420.34,252.93L420.31,252.88C417.55,248.8 413.77,245.35 409.45,242.91Z"
android:fillColor="#37BF6E"
android:fillType="nonZero"/>
<path
android:pathData="M381.24,277.47C381.51,277.92 381.77,278.38 382.01,278.84C382.21,279.24 382.39,279.65 382.57,280.06C382.91,280.88 383.19,281.73 383.41,282.59C383.74,283.88 383.92,285.21 383.93,286.57L383.93,361.1C383.96,363.95 383.35,366.77 382.16,369.36C381.93,369.86 381.69,370.35 381.42,370.83C379.75,373.79 377.32,376.27 374.39,378L310.2,415.87C307.47,417.48 304.38,418.39 301.21,418.53L301.22,439.23C301.22,439.23 301.24,449.84 301.27,464.74C306.1,464.61 310.91,463.3 315.21,460.75L410.98,404.25C419.88,399 425.31,389.37 425.22,379.03L425.22,267.85C425.17,262.48 423.34,257.34 420.34,252.93L397.79,266.39L381.24,277.47Z"
android:fillColor="#3870B2"
android:fillType="nonZero"/>
<path
android:pathData="M177.75,256.17C179.93,251.62 183.31,247.73 187.92,245L283.68,188.51C292.58,183.26 303.64,183.15 312.64,188.23L409.45,242.91C413.77,245.35 417.55,248.8 420.31,252.88L420.34,252.93L498.59,206.19C494.03,199.46 487.79,193.78 480.67,189.75L320.86,99.49C306.01,91.1 287.75,91.27 273.07,99.95L114.99,193.2C107.39,197.69 101.81,204.11 98.21,211.63L177.74,256.19L177.75,256.17ZM301.27,464.74C301.09,464.74 300.91,464.76 300.73,464.76C295.66,464.8 290.67,463.51 286.26,461.02L184.53,403.56C178.57,400.2 174.85,393.89 174.79,387.05L174.78,270.22C174.73,265.23 175.72,260.43 177.74,256.19L98.21,211.63C94.86,218.63 93.23,226.58 93.31,234.82L93.31,427.67C93.42,438.97 99.54,449.37 109.4,454.92L277.31,549.77C284.6,553.88 292.84,556.01 301.2,555.94L301.2,555.8C301.39,543.78 301.33,495.26 301.27,464.74Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
<path
android:pathData="M498.59,206.19L420.34,252.93C423.34,257.34 425.17,262.48 425.22,267.85L425.22,379.03C425.31,389.37 419.88,399 410.98,404.25L315.21,460.75C310.91,463.3 306.1,464.61 301.27,464.74C301.33,495.26 301.39,543.78 301.2,555.8L301.2,555.94C309.48,555.87 317.74,553.68 325.11,549.32L483.18,456.06C497.87,447.39 506.85,431.49 506.69,414.43L506.69,230.91C506.6,222.02 503.57,213.5 498.59,206.19Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
<path
android:pathData="M301.2,555.94C292.84,556.01 284.6,553.88 277.31,549.76L109.4,454.92C99.54,449.37 93.42,438.97 93.31,427.67L93.31,234.82C93.23,226.58 94.86,218.63 98.21,211.63C101.81,204.11 107.39,197.69 114.99,193.2L273.07,99.95C287.75,91.27 306.01,91.1 320.86,99.49L480.67,189.75C487.79,193.78 494.03,199.46 498.59,206.19C503.57,213.5 506.6,222.02 506.69,230.91L506.69,414.43C506.85,431.49 497.87,447.39 483.18,456.06L325.11,549.32C317.74,553.68 309.48,555.87 301.2,555.94Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
</vector>

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

@ -0,0 +1,36 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="600dp"
android:height="600dp"
android:viewportWidth="600"
android:viewportHeight="600">
<path
android:pathData="M301.21,418.53C300.97,418.54 300.73,418.56 300.49,418.56C297.09,418.59 293.74,417.72 290.79,416.05L222.6,377.54C220.63,376.43 219,374.82 217.85,372.88C216.7,370.94 216.09,368.73 216.07,366.47L216.07,288.16C216.06,287.32 216.09,286.49 216.17,285.67C216.38,283.54 216.91,281.5 217.71,279.6L199.29,268.27L177.74,256.19C175.72,260.43 174.73,265.23 174.78,270.22L174.79,387.05C174.85,393.89 178.57,400.2 184.53,403.56L286.26,461.02C290.67,463.51 295.66,464.8 300.73,464.76C300.91,464.76 301.09,464.74 301.27,464.74C301.24,449.84 301.22,439.23 301.22,439.23L301.21,418.53Z"
android:fillColor="#041619"
android:fillType="nonZero"/>
<path
android:pathData="M409.45,242.91L312.64,188.23C303.64,183.15 292.58,183.26 283.68,188.51L187.92,245C183.31,247.73 179.93,251.62 177.75,256.17L177.74,256.19L199.29,268.27L217.71,279.6C217.83,279.32 217.92,279.02 218.05,278.74C218.24,278.36 218.43,277.98 218.64,277.62C219.06,276.88 219.52,276.18 220.04,275.51C221.37,273.8 223.01,272.35 224.87,271.25L289.06,233.39C290.42,232.59 291.87,231.96 293.39,231.51C295.53,230.87 297.77,230.6 300,230.72C302.98,230.88 305.88,231.73 308.47,233.2L373.37,269.85C375.54,271.08 377.49,272.68 379.13,274.57C379.68,275.19 380.18,275.85 380.65,276.53C380.86,276.84 381.05,277.15 381.24,277.47L397.79,266.39L420.34,252.93L420.31,252.88C417.55,248.8 413.77,245.35 409.45,242.91Z"
android:fillColor="#37BF6E"
android:fillType="nonZero"/>
<path
android:pathData="M381.24,277.47C381.51,277.92 381.77,278.38 382.01,278.84C382.21,279.24 382.39,279.65 382.57,280.06C382.91,280.88 383.19,281.73 383.41,282.59C383.74,283.88 383.92,285.21 383.93,286.57L383.93,361.1C383.96,363.95 383.35,366.77 382.16,369.36C381.93,369.86 381.69,370.35 381.42,370.83C379.75,373.79 377.32,376.27 374.39,378L310.2,415.87C307.47,417.48 304.38,418.39 301.21,418.53L301.22,439.23C301.22,439.23 301.24,449.84 301.27,464.74C306.1,464.61 310.91,463.3 315.21,460.75L410.98,404.25C419.88,399 425.31,389.37 425.22,379.03L425.22,267.85C425.17,262.48 423.34,257.34 420.34,252.93L397.79,266.39L381.24,277.47Z"
android:fillColor="#3870B2"
android:fillType="nonZero"/>
<path
android:pathData="M177.75,256.17C179.93,251.62 183.31,247.73 187.92,245L283.68,188.51C292.58,183.26 303.64,183.15 312.64,188.23L409.45,242.91C413.77,245.35 417.55,248.8 420.31,252.88L420.34,252.93L498.59,206.19C494.03,199.46 487.79,193.78 480.67,189.75L320.86,99.49C306.01,91.1 287.75,91.27 273.07,99.95L114.99,193.2C107.39,197.69 101.81,204.11 98.21,211.63L177.74,256.19L177.75,256.17ZM301.27,464.74C301.09,464.74 300.91,464.76 300.73,464.76C295.66,464.8 290.67,463.51 286.26,461.02L184.53,403.56C178.57,400.2 174.85,393.89 174.79,387.05L174.78,270.22C174.73,265.23 175.72,260.43 177.74,256.19L98.21,211.63C94.86,218.63 93.23,226.58 93.31,234.82L93.31,427.67C93.42,438.97 99.54,449.37 109.4,454.92L277.31,549.77C284.6,553.88 292.84,556.01 301.2,555.94L301.2,555.8C301.39,543.78 301.33,495.26 301.27,464.74Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
<path
android:pathData="M498.59,206.19L420.34,252.93C423.34,257.34 425.17,262.48 425.22,267.85L425.22,379.03C425.31,389.37 419.88,399 410.98,404.25L315.21,460.75C310.91,463.3 306.1,464.61 301.27,464.74C301.33,495.26 301.39,543.78 301.2,555.8L301.2,555.94C309.48,555.87 317.74,553.68 325.11,549.32L483.18,456.06C497.87,447.39 506.85,431.49 506.69,414.43L506.69,230.91C506.6,222.02 503.57,213.5 498.59,206.19Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
<path
android:pathData="M301.2,555.94C292.84,556.01 284.6,553.88 277.31,549.76L109.4,454.92C99.54,449.37 93.42,438.97 93.31,427.67L93.31,234.82C93.23,226.58 94.86,218.63 98.21,211.63C101.81,204.11 107.39,197.69 114.99,193.2L273.07,99.95C287.75,91.27 306.01,91.1 320.86,99.49L480.67,189.75C487.79,193.78 494.03,199.46 498.59,206.19C503.57,213.5 506.6,222.02 506.69,230.91L506.69,414.43C506.85,431.49 497.87,447.39 483.18,456.06L325.11,549.32C317.74,553.68 309.48,555.87 301.2,555.94Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
</vector>

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

@ -0,0 +1,36 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="600dp"
android:height="600dp"
android:viewportWidth="600"
android:viewportHeight="600">
<path
android:pathData="M301.21,418.53C300.97,418.54 300.73,418.56 300.49,418.56C297.09,418.59 293.74,417.72 290.79,416.05L222.6,377.54C220.63,376.43 219,374.82 217.85,372.88C216.7,370.94 216.09,368.73 216.07,366.47L216.07,288.16C216.06,287.32 216.09,286.49 216.17,285.67C216.38,283.54 216.91,281.5 217.71,279.6L199.29,268.27L177.74,256.19C175.72,260.43 174.73,265.23 174.78,270.22L174.79,387.05C174.85,393.89 178.57,400.2 184.53,403.56L286.26,461.02C290.67,463.51 295.66,464.8 300.73,464.76C300.91,464.76 301.09,464.74 301.27,464.74C301.24,449.84 301.22,439.23 301.22,439.23L301.21,418.53Z"
android:fillColor="#041619"
android:fillType="nonZero"/>
<path
android:pathData="M409.45,242.91L312.64,188.23C303.64,183.15 292.58,183.26 283.68,188.51L187.92,245C183.31,247.73 179.93,251.62 177.75,256.17L177.74,256.19L199.29,268.27L217.71,279.6C217.83,279.32 217.92,279.02 218.05,278.74C218.24,278.36 218.43,277.98 218.64,277.62C219.06,276.88 219.52,276.18 220.04,275.51C221.37,273.8 223.01,272.35 224.87,271.25L289.06,233.39C290.42,232.59 291.87,231.96 293.39,231.51C295.53,230.87 297.77,230.6 300,230.72C302.98,230.88 305.88,231.73 308.47,233.2L373.37,269.85C375.54,271.08 377.49,272.68 379.13,274.57C379.68,275.19 380.18,275.85 380.65,276.53C380.86,276.84 381.05,277.15 381.24,277.47L397.79,266.39L420.34,252.93L420.31,252.88C417.55,248.8 413.77,245.35 409.45,242.91Z"
android:fillColor="#37BF6E"
android:fillType="nonZero"/>
<path
android:pathData="M381.24,277.47C381.51,277.92 381.77,278.38 382.01,278.84C382.21,279.24 382.39,279.65 382.57,280.06C382.91,280.88 383.19,281.73 383.41,282.59C383.74,283.88 383.92,285.21 383.93,286.57L383.93,361.1C383.96,363.95 383.35,366.77 382.16,369.36C381.93,369.86 381.69,370.35 381.42,370.83C379.75,373.79 377.32,376.27 374.39,378L310.2,415.87C307.47,417.48 304.38,418.39 301.21,418.53L301.22,439.23C301.22,439.23 301.24,449.84 301.27,464.74C306.1,464.61 310.91,463.3 315.21,460.75L410.98,404.25C419.88,399 425.31,389.37 425.22,379.03L425.22,267.85C425.17,262.48 423.34,257.34 420.34,252.93L397.79,266.39L381.24,277.47Z"
android:fillColor="#3870B2"
android:fillType="nonZero"/>
<path
android:pathData="M177.75,256.17C179.93,251.62 183.31,247.73 187.92,245L283.68,188.51C292.58,183.26 303.64,183.15 312.64,188.23L409.45,242.91C413.77,245.35 417.55,248.8 420.31,252.88L420.34,252.93L498.59,206.19C494.03,199.46 487.79,193.78 480.67,189.75L320.86,99.49C306.01,91.1 287.75,91.27 273.07,99.95L114.99,193.2C107.39,197.69 101.81,204.11 98.21,211.63L177.74,256.19L177.75,256.17ZM301.27,464.74C301.09,464.74 300.91,464.76 300.73,464.76C295.66,464.8 290.67,463.51 286.26,461.02L184.53,403.56C178.57,400.2 174.85,393.89 174.79,387.05L174.78,270.22C174.73,265.23 175.72,260.43 177.74,256.19L98.21,211.63C94.86,218.63 93.23,226.58 93.31,234.82L93.31,427.67C93.42,438.97 99.54,449.37 109.4,454.92L277.31,549.77C284.6,553.88 292.84,556.01 301.2,555.94L301.2,555.8C301.39,543.78 301.33,495.26 301.27,464.74Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
<path
android:pathData="M498.59,206.19L420.34,252.93C423.34,257.34 425.17,262.48 425.22,267.85L425.22,379.03C425.31,389.37 419.88,399 410.98,404.25L315.21,460.75C310.91,463.3 306.1,464.61 301.27,464.74C301.33,495.26 301.39,543.78 301.2,555.8L301.2,555.94C309.48,555.87 317.74,553.68 325.11,549.32L483.18,456.06C497.87,447.39 506.85,431.49 506.69,414.43L506.69,230.91C506.6,222.02 503.57,213.5 498.59,206.19Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
<path
android:pathData="M301.2,555.94C292.84,556.01 284.6,553.88 277.31,549.76L109.4,454.92C99.54,449.37 93.42,438.97 93.31,427.67L93.31,234.82C93.23,226.58 94.86,218.63 98.21,211.63C101.81,204.11 107.39,197.69 114.99,193.2L273.07,99.95C287.75,91.27 306.01,91.1 320.86,99.49L480.67,189.75C487.79,193.78 494.03,199.46 498.59,206.19C503.57,213.5 506.6,222.02 506.69,230.91L506.69,414.43C506.85,431.49 497.87,447.39 483.18,456.06L325.11,549.32C317.74,553.68 309.48,555.87 301.2,555.94Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
</vector>

13
gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/values/strings.xml

@ -0,0 +1,13 @@
<resources>
<string name="app_name">Compose Resources App</string>
<string name="hello">😊 Hello world!</string>
<string name="multi_line">Lorem ipsum dolor sit amet,
consectetur adipiscing elit.
Donec eget turpis ac sem ultricies consequat.</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>

0
gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/ignored_2.txt

2
gradle-plugins/gradle.properties

@ -11,7 +11,7 @@ compose.tests.compiler.compatible.kotlin.version=1.9.21
compose.tests.js.compiler.compatible.kotlin.version=1.9.21 compose.tests.js.compiler.compatible.kotlin.version=1.9.21
# __SUPPORTED_GRADLE_VERSIONS__ # __SUPPORTED_GRADLE_VERSIONS__
# Don't forget to edit versions in .github/workflows/gradle-plugin.yml as well # Don't forget to edit versions in .github/workflows/gradle-plugin.yml as well
compose.tests.gradle.versions=7.3.3, 8.3 compose.tests.gradle.versions=7.4, 8.3
# A version of Gradle plugin, that will be published, # A version of Gradle plugin, that will be published,
# unless overridden by COMPOSE_GRADLE_PLUGIN_VERSION env var. # unless overridden by COMPOSE_GRADLE_PLUGIN_VERSION env var.

11
gradle-plugins/gradle/libs.versions.toml

@ -1,12 +1,19 @@
[versions] [versions]
kotlin = "1.9.0" kotlin = "1.9.0"
gradle-download-plugin = "5.5.0" gradle-download-plugin = "5.5.0"
kotlin-poet = "1.14.2"
plugin-android = "7.3.0"
shadow-jar = "8.1.1"
publish-plugin = "1.2.1"
[libraries] [libraries]
download-task = { module = "de.undercouch:gradle-download-task", version.ref = "gradle-download-plugin" } download-task = { module = "de.undercouch:gradle-download-task", version.ref = "gradle-download-plugin" }
kotlin-poet = { module = "com.squareup:kotlinpoet", version.ref = "kotlin-poet" }
plugin-android = { module = "com.android.tools.build:gradle", version.ref = "plugin-android" }
plugin-android-api = { module = "com.android.tools.build:gradle-api", version.ref = "plugin-android" }
[plugins] [plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
shadow-jar = "com.github.johnrengelman.shadow:8.1.1"
download = { id = "de.undercouch.download", version.ref = "gradle-download-plugin" } download = { id = "de.undercouch.download", version.ref = "gradle-download-plugin" }
publish-plugin-portal = "com.gradle.plugin-publish:1.2.1" shadow-jar = { id = "com.github.johnrengelman.shadow", version.ref = "shadow-jar" }
publish-plugin = { id = "com.gradle.plugin-publish", version.ref = "publish-plugin" }

Loading…
Cancel
Save