diff --git a/.github/workflows/gradle-plugin.yml b/.github/workflows/gradle-plugin.yml index f54484e93b..dc1274f2eb 100644 --- a/.github/workflows/gradle-plugin.yml +++ b/.github/workflows/gradle-plugin.yml @@ -10,7 +10,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-20.04, macos-12, windows-2022] - gradle: [7.3.3, 8.3] + gradle: [7.4, 8.3] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 diff --git a/components/gradle.properties b/components/gradle.properties index 69b07d65ad..cad7f7bb93 100644 --- a/components/gradle.properties +++ b/components/gradle.properties @@ -15,6 +15,7 @@ agp.version=8.1.2 org.jetbrains.compose.experimental.jscanvas.enabled=true org.jetbrains.compose.experimental.macos.enabled=true org.jetbrains.compose.experimental.uikit.enabled=true +compose.resources.always.generate.accessors=true compose.desktop.verbose=true compose.useMavenLocal=false diff --git a/components/resources/demo/shared/build.gradle.kts b/components/resources/demo/shared/build.gradle.kts index e3c500f0f7..521e303e38 100644 --- a/components/resources/demo/shared/build.gradle.kts +++ b/components/resources/demo/shared/build.gradle.kts @@ -78,11 +78,6 @@ android { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } - sourceSets { - named("main") { - resources.srcDir("src/commonMain/resources") - } - } } compose.experimental { diff --git a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt index 2d6aab445f..f7a3877432 100644 --- a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt +++ b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt @@ -28,7 +28,7 @@ fun FileRes(paddingValues: PaddingValues) { ) { Text( modifier = Modifier.padding(16.dp), - text = "File: 'images/droid_icon.xml'", + text = "File: 'composeRes/images/droid_icon.xml'", style = MaterialTheme.typography.titleLarge ) OutlinedCard( @@ -38,7 +38,7 @@ fun FileRes(paddingValues: PaddingValues) { ) { var bytes by remember { mutableStateOf(ByteArray(0)) } LaunchedEffect(Unit) { - bytes = readResourceBytes("images/droid_icon.xml") + bytes = readResourceBytes("composeRes/images/droid_icon.xml") } Text( modifier = Modifier.padding(8.dp).height(200.dp).verticalScroll(rememberScrollState()), @@ -54,7 +54,7 @@ fun FileRes(paddingValues: PaddingValues) { mutableStateOf(ByteArray(0)) } LaunchedEffect(Unit) { - bytes = readBytes("images/droid_icon.xml") + bytes = readBytes("composeRes/images/droid_icon.xml") } Text(bytes.decodeToString()) """.trimIndent() diff --git a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FontRes.kt b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FontRes.kt index 55cb969388..eaedd84ce1 100644 --- a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FontRes.kt +++ b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FontRes.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp +import components.resources.demo.generated.resources.Res import org.jetbrains.compose.resources.Font @Composable @@ -28,7 +29,7 @@ fun FontRes(paddingValues: PaddingValues) { Text( modifier = Modifier.padding(8.dp), 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) Text( 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) Text( modifier = Modifier.padding(16.dp), diff --git a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/ImagesRes.kt b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/ImagesRes.kt index 73c81ed419..fcb0d1a6db 100644 --- a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/ImagesRes.kt +++ b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/ImagesRes.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import components.resources.demo.generated.resources.Res import org.jetbrains.compose.resources.imageResource import org.jetbrains.compose.resources.vectorResource import org.jetbrains.compose.resources.painterResource @@ -27,13 +28,13 @@ fun ImagesRes(contentPadding: PaddingValues) { ) { Image( modifier = Modifier.size(100.dp), - painter = painterResource("images/compose.png"), + painter = painterResource(Res.images.compose), contentDescription = null ) Text( """ Image( - painter = painterResource("images/compose.png") + painter = painterResource(Res.images.compose) ) """.trimIndent() ) @@ -46,13 +47,13 @@ fun ImagesRes(contentPadding: PaddingValues) { ) { Image( modifier = Modifier.size(100.dp), - painter = painterResource("images/insta_icon.xml"), + painter = painterResource(Res.images.insta_icon), contentDescription = null ) Text( """ Image( - painter = painterResource("images/insta_icon.xml") + painter = painterResource(Res.images.insta_icon) ) """.trimIndent() ) @@ -65,13 +66,13 @@ fun ImagesRes(contentPadding: PaddingValues) { ) { Image( modifier = Modifier.size(140.dp), - bitmap = imageResource("images/land.webp"), + bitmap = imageResource(Res.images.land), contentDescription = null ) Text( """ Image( - bitmap = imageResource("images/land.webp") + bitmap = imageResource(Res.images.land) ) """.trimIndent() ) @@ -84,13 +85,13 @@ fun ImagesRes(contentPadding: PaddingValues) { ) { Image( modifier = Modifier.size(100.dp), - imageVector = vectorResource("images/droid_icon.xml"), + imageVector = vectorResource(Res.images.droid_icon), contentDescription = null ) Text( """ Image( - imageVector = vectorResource("images/droid_icon.xml") + imageVector = vectorResource(Res.images.droid_icon) ) """.trimIndent() ) @@ -103,13 +104,13 @@ fun ImagesRes(contentPadding: PaddingValues) { ) { Icon( modifier = Modifier.size(100.dp), - painter = painterResource("images/compose.png"), + painter = painterResource(Res.images.compose), contentDescription = null ) Text( """ Icon( - painter = painterResource("images/compose.png") + painter = painterResource(Res.images.compose) ) """.trimIndent() ) @@ -122,13 +123,13 @@ fun ImagesRes(contentPadding: PaddingValues) { ) { Icon( modifier = Modifier.size(100.dp), - painter = painterResource("images/insta_icon.xml"), + painter = painterResource(Res.images.insta_icon), contentDescription = null ) Text( """ Icon( - painter = painterResource("images/insta_icon.xml") + painter = painterResource(Res.images.insta_icon) ) """.trimIndent() ) @@ -141,13 +142,13 @@ fun ImagesRes(contentPadding: PaddingValues) { ) { Icon( modifier = Modifier.size(140.dp), - bitmap = imageResource("images/land.webp"), + bitmap = imageResource(Res.images.land), contentDescription = null ) Text( """ Icon( - bitmap = imageResource("images/land.webp") + bitmap = imageResource(Res.images.land) ) """.trimIndent() ) @@ -160,13 +161,13 @@ fun ImagesRes(contentPadding: PaddingValues) { ) { Icon( modifier = Modifier.size(100.dp), - imageVector = vectorResource("images/droid_icon.xml"), + imageVector = vectorResource(Res.images.droid_icon), contentDescription = null ) Text( """ Icon( - imageVector = vectorResource("images/droid_icon.xml") + imageVector = vectorResource(Res.images.droid_icon) ) """.trimIndent() ) diff --git a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt index 7ba300ba6f..d579ce69b4 100644 --- a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt +++ b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import components.resources.demo.generated.resources.Res import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.getStringArray import org.jetbrains.compose.resources.readResourceBytes @@ -32,7 +33,7 @@ fun StringRes(paddingValues: PaddingValues) { ) { Text( modifier = Modifier.padding(16.dp), - text = "strings.xml", + text = "composeRes/values/strings.xml", style = MaterialTheme.typography.titleLarge ) OutlinedCard( @@ -42,7 +43,7 @@ fun StringRes(paddingValues: PaddingValues) { ) { var bytes by remember { mutableStateOf(ByteArray(0)) } LaunchedEffect(Unit) { - bytes = readResourceBytes("strings.xml") + bytes = readResourceBytes("composeRes/values/strings.xml") } Text( modifier = Modifier.padding(8.dp), @@ -53,9 +54,9 @@ fun StringRes(paddingValues: PaddingValues) { } OutlinedTextField( modifier = Modifier.padding(16.dp).fillMaxWidth(), - value = getString("app_name"), + value = getString(Res.strings.app_name), onValueChange = {}, - label = { Text("Text(getString(\"app_name\"))") }, + label = { Text("Text(getString(Res.strings.app_name)") }, enabled = false, colors = TextFieldDefaults.colors( disabledTextColor = MaterialTheme.colorScheme.onSurface, @@ -65,9 +66,9 @@ fun StringRes(paddingValues: PaddingValues) { ) OutlinedTextField( modifier = Modifier.padding(16.dp).fillMaxWidth(), - value = getString("hello"), + value = getString(Res.strings.hello), onValueChange = {}, - label = { Text("Text(getString(\"hello\"))") }, + label = { Text("Text(getString(Res.strings.hello)") }, enabled = false, colors = TextFieldDefaults.colors( disabledTextColor = MaterialTheme.colorScheme.onSurface, @@ -77,9 +78,9 @@ fun StringRes(paddingValues: PaddingValues) { ) OutlinedTextField( modifier = Modifier.padding(16.dp).fillMaxWidth(), - value = getString("multi_line"), + value = getString(Res.strings.multi_line), onValueChange = {}, - label = { Text("Text(getString(\"multi_line\"))") }, + label = { Text("Text(getString(Res.strings.multi_line)") }, enabled = false, colors = TextFieldDefaults.colors( disabledTextColor = MaterialTheme.colorScheme.onSurface, @@ -89,9 +90,9 @@ fun StringRes(paddingValues: PaddingValues) { ) OutlinedTextField( modifier = Modifier.padding(16.dp).fillMaxWidth(), - value = getString("str_template", "User_name", 100), + value = getString(Res.strings.str_template, "User_name", 100), onValueChange = {}, - label = { Text("Text(getString(\"str_template\", \"User_name\", 100))") }, + label = { Text("Text(getString(Res.strings.str_template, \"User_name\", 100)") }, enabled = false, colors = TextFieldDefaults.colors( disabledTextColor = MaterialTheme.colorScheme.onSurface, @@ -101,9 +102,9 @@ fun StringRes(paddingValues: PaddingValues) { ) OutlinedTextField( modifier = Modifier.padding(16.dp).fillMaxWidth(), - value = getStringArray("str_arr").toString(), + value = getStringArray(Res.strings.str_arr).toString(), onValueChange = {}, - label = { Text("Text(getStringArray(\"str_arr\").toString())") }, + label = { Text("Text(getStringArray(Res.strings.str_arr).toString())") }, enabled = false, colors = TextFieldDefaults.colors( disabledTextColor = MaterialTheme.colorScheme.onSurface, diff --git a/components/resources/demo/shared/src/androidMain/assets/font_awesome.otf b/components/resources/demo/shared/src/commonMain/resources/composeRes/fonts/font_awesome.otf similarity index 100% rename from components/resources/demo/shared/src/androidMain/assets/font_awesome.otf rename to components/resources/demo/shared/src/commonMain/resources/composeRes/fonts/font_awesome.otf diff --git a/components/resources/demo/shared/src/commonMain/resources/images/compose.png b/components/resources/demo/shared/src/commonMain/resources/composeRes/images/compose.png similarity index 100% rename from components/resources/demo/shared/src/commonMain/resources/images/compose.png rename to components/resources/demo/shared/src/commonMain/resources/composeRes/images/compose.png diff --git a/components/resources/demo/shared/src/commonMain/resources/images/droid_icon.xml b/components/resources/demo/shared/src/commonMain/resources/composeRes/images/droid_icon.xml similarity index 100% rename from components/resources/demo/shared/src/commonMain/resources/images/droid_icon.xml rename to components/resources/demo/shared/src/commonMain/resources/composeRes/images/droid_icon.xml diff --git a/components/resources/demo/shared/src/commonMain/resources/images/insta_icon.xml b/components/resources/demo/shared/src/commonMain/resources/composeRes/images/insta_icon.xml similarity index 100% rename from components/resources/demo/shared/src/commonMain/resources/images/insta_icon.xml rename to components/resources/demo/shared/src/commonMain/resources/composeRes/images/insta_icon.xml diff --git a/components/resources/demo/shared/src/commonMain/resources/images/land.webp b/components/resources/demo/shared/src/commonMain/resources/composeRes/images/land.webp similarity index 100% rename from components/resources/demo/shared/src/commonMain/resources/images/land.webp rename to components/resources/demo/shared/src/commonMain/resources/composeRes/images/land.webp diff --git a/components/resources/demo/shared/src/commonMain/resources/strings.xml b/components/resources/demo/shared/src/commonMain/resources/composeRes/values/strings.xml similarity index 100% rename from components/resources/demo/shared/src/commonMain/resources/strings.xml rename to components/resources/demo/shared/src/commonMain/resources/composeRes/values/strings.xml diff --git a/components/resources/demo/shared/src/commonMain/resources/font_awesome.otf b/components/resources/demo/shared/src/commonMain/resources/font_awesome.otf deleted file mode 100644 index 401ec0f36e..0000000000 Binary files a/components/resources/demo/shared/src/commonMain/resources/font_awesome.otf and /dev/null differ diff --git a/components/resources/library/src/androidInstrumentedTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.android.kt b/components/resources/library/src/androidInstrumentedTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.android.kt index 5a88b45ccc..aed5d003a4 100644 --- a/components/resources/library/src/androidInstrumentedTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.android.kt +++ b/components/resources/library/src/androidInstrumentedTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.android.kt @@ -25,17 +25,17 @@ class ComposeResourceTest { @Test fun testCountRecompositions() = runComposeUiTest { runBlockingTest { - val imagePathFlow = MutableStateFlow("1.png") + val imagePathFlow = MutableStateFlow(ImageResource("1.png")) val recompositionsCounter = RecompositionsCounter() setContent { - val path by imagePathFlow.collectAsState() - val res = imageResource(path) + val res by imagePathFlow.collectAsState() + val imgRes = imageResource(res) recompositionsCounter.content { - Image(bitmap = res, contentDescription = null) + Image(bitmap = imgRes, contentDescription = null) } } awaitIdle() - imagePathFlow.emit("2.png") + imagePathFlow.emit(ImageResource("2.png")) awaitIdle() assertEquals(2, recompositionsCounter.count) } @@ -45,17 +45,17 @@ class ComposeResourceTest { fun testImageResourceCache() = runComposeUiTest { runBlockingTest { val testResourceReader = TestResourceReader() - val imagePathFlow = MutableStateFlow("1.png") + val imagePathFlow = MutableStateFlow(ImageResource("1.png")) setContent { CompositionLocalProvider(LocalResourceReader provides testResourceReader) { - val path by imagePathFlow.collectAsState() - Image(painterResource(path), null) + val res by imagePathFlow.collectAsState() + Image(painterResource(res), null) } } awaitIdle() - imagePathFlow.emit("2.png") + imagePathFlow.emit(ImageResource("2.png")) awaitIdle() - imagePathFlow.emit("1.png") + imagePathFlow.emit(ImageResource("1.png")) awaitIdle() assertEquals( @@ -69,18 +69,18 @@ class ComposeResourceTest { fun testStringResourceCache() = runComposeUiTest { runBlockingTest { val testResourceReader = TestResourceReader() - val stringIdFlow = MutableStateFlow("app_name") + val stringIdFlow = MutableStateFlow(TestStringResource("app_name")) setContent { CompositionLocalProvider(LocalResourceReader provides testResourceReader) { - val textId by stringIdFlow.collectAsState() - Text(getString(textId)) - Text(getStringArray("str_arr").joinToString()) + val res by stringIdFlow.collectAsState() + Text(getString(res)) + Text(getStringArray(TestStringResource("str_arr")).joinToString()) } } awaitIdle() - stringIdFlow.emit("hello") + stringIdFlow.emit(TestStringResource("hello")) awaitIdle() - stringIdFlow.emit("app_name") + stringIdFlow.emit(TestStringResource("app_name")) awaitIdle() assertEquals( @@ -94,12 +94,12 @@ class ComposeResourceTest { fun testReadStringResource() = runComposeUiTest { runBlockingTest { setContent { - assertEquals("Compose Resources App", getString("app_name")) + assertEquals("Compose Resources App", getString(TestStringResource("app_name"))) assertEquals( "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() } diff --git a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt index 8856f503fc..ab11f6a7db 100644 --- a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt +++ b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt @@ -1,7 +1,6 @@ package org.jetbrains.compose.resources import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontStyle @@ -9,7 +8,7 @@ import androidx.compose.ui.text.font.FontWeight @ExperimentalResourceApi @Composable -actual fun Font(id: ResourceId, weight: FontWeight, style: FontStyle): Font { - val path by rememberState(id, { "" }) { getPathById(id) } +actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): Font { + val path = resource.getPathByEnvironment() return Font(path, LocalContext.current.assets, weight, style) } \ No newline at end of file diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt index af61383de5..cc2e80cf52 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt @@ -1,14 +1,37 @@ package org.jetbrains.compose.resources import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight /** - * 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): 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 style The style of the font. Default value is [FontStyle.Normal]. * @@ -19,7 +42,7 @@ import androidx.compose.ui.text.font.FontWeight @ExperimentalResourceApi @Composable expect fun Font( - id: ResourceId, + resource: FontResource, weight: FontWeight = FontWeight.Normal, style: FontStyle = FontStyle.Normal ): Font \ No newline at end of file diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt index c2989babea..5a5bd1bc1e 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt @@ -1,7 +1,9 @@ package org.jetbrains.compose.resources import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter 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.platform.LocalDensity import androidx.compose.ui.unit.dp -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.withContext +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import org.jetbrains.compose.resources.vector.toImageVector 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) : 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. * - * @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. */ @ExperimentalResourceApi @Composable -fun painterResource(id: ResourceId): Painter { - val filePath by rememberFilePath(id) +fun painterResource(resource: ImageResource): Painter { + val filePath = remember(resource) { resource.getPathByEnvironment() } val isXml = filePath.endsWith(".xml", true) if (isXml) { - return rememberVectorPainter(vectorResource(id)) + return rememberVectorPainter(vectorResource(resource)) } else { - return BitmapPainter(imageResource(id)) + return BitmapPainter(imageResource(resource)) } } 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. */ @ExperimentalResourceApi @Composable -fun imageResource(id: ResourceId): ImageBitmap { +fun imageResource(resource: ImageResource): ImageBitmap { val resourceReader = LocalResourceReader.current - val imageBitmap by rememberState(id, { emptyImageBitmap }) { - val path = getPathById(id) + val imageBitmap by rememberState(resource, { emptyImageBitmap }) { + val path = resource.getPathByEnvironment() val cached = loadImage(path, resourceReader) { ImageCache.Bitmap(it.toImageBitmap()) } 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. */ @ExperimentalResourceApi @Composable -fun vectorResource(id: ResourceId): ImageVector { +fun vectorResource(resource: ImageResource): ImageVector { val resourceReader = LocalResourceReader.current val density = LocalDensity.current - val imageVector by rememberState(id, { emptyImageVector }) { - val path = getPathById(id) + val imageVector by rememberState(resource, { emptyImageVector }) { + val path = resource.getPathByEnvironment() val cached = loadImage(path, resourceReader) { ImageCache.Vector(it.toXmlElement().toImageVector(density)) } as ImageCache.Vector @@ -90,9 +114,8 @@ private sealed interface ImageCache { class Vector(val vector: ImageVector) : ImageCache } -@OptIn(ExperimentalCoroutinesApi::class) -private val imageCacheDispatcher = Dispatchers.Default.limitedParallelism(1) -private val imageCache = mutableMapOf() +private val imageCacheMutex = Mutex() +private val imageCache = mutableMapOf>() //@TestOnly internal fun dropImageCache() { @@ -103,6 +126,14 @@ private suspend fun loadImage( path: String, resourceReader: ResourceReader, decode: (ByteArray) -> ImageCache -): ImageCache = withContext(imageCacheDispatcher) { - imageCache.getOrPut(path) { decode(resourceReader.read(path)) } +): ImageCache = coroutineScope { + 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() } diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.kt index 3928a4b1dd..a4e100e58f 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.kt @@ -1,6 +1,48 @@ 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.") -annotation class ExperimentalResourceApi \ No newline at end of file +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 +) { + 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, + internal val path: String +) + +internal fun Resource.getPathByEnvironment(): String { + //TODO + return items.first().path +} \ No newline at end of file diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceIndex.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceIndex.kt deleted file mode 100644 index c921974821..0000000000 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceIndex.kt +++ /dev/null @@ -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 = - rememberState(id, { "" }) { getPathById(id) } - -internal val ResourceId.stringKey get() = this \ No newline at end of file diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt index 194db39a78..32e797b4b5 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt @@ -1,85 +1,110 @@ package org.jetbrains.compose.resources import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.withContext +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import org.jetbrains.compose.resources.vector.xmldom.Element import org.jetbrains.compose.resources.vector.xmldom.NodeList -private const val STRINGS_XML = "strings.xml" //todo 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): Resource(id, items) + private sealed interface StringItem { data class Value(val text: String) : StringItem data class Array(val items: List) : StringItem } -@OptIn(ExperimentalCoroutinesApi::class) -private val stringsCacheDispatcher = Dispatchers.Default.limitedParallelism(1) -private val parsedStringsCache = mutableMapOf>() +private val stringsCacheMutex = Mutex() +private val parsedStringsCache = mutableMapOf>>() //@TestOnly internal fun dropStringsCache() { parsedStringsCache.clear() } -private suspend fun getParsedStrings(path: String, resourceReader: ResourceReader): Map = - withContext(stringsCacheDispatcher) { +private suspend fun getParsedStrings( + path: String, + resourceReader: ResourceReader +): Map = coroutineScope { + val deferred = stringsCacheMutex.withLock { parsedStringsCache.getOrPut(path) { - val nodes = resourceReader.read(path).toXmlElement().childNodes - val strings = nodes.getElementsWithName("string").associate { element -> - element.getAttribute("name") to StringItem.Value(element.textContent.orEmpty()) - } - val arrays = nodes.getElementsWithName("string-array").associate { arrayElement -> - val items = arrayElement.childNodes.getElementsWithName("item").map { element -> - element.textContent.orEmpty() - } - arrayElement.getAttribute("name") to StringItem.Array(items) + //LAZY - to free the mutex lock as fast as possible + async(start = CoroutineStart.LAZY) { + parseStringXml(path, resourceReader) } - strings + arrays } } + deferred.await() +} + +private suspend fun parseStringXml(path: String, resourceReader: ResourceReader): Map { + val nodes = resourceReader.read(path).toXmlElement().childNodes + val strings = nodes.getElementsWithName("string").associate { element -> + element.getAttribute("name") to StringItem.Value(element.textContent.orEmpty()) + } + val arrays = nodes.getElementsWithName("string-array").associate { arrayElement -> + val items = arrayElement.childNodes.getElementsWithName("item").map { element -> + element.textContent.orEmpty() + } + arrayElement.getAttribute("name") to StringItem.Array(items) + } + 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. * * @throws IllegalArgumentException If the provided ID is not found in the resource file. */ @ExperimentalResourceApi @Composable -fun getString(id: ResourceId): String { +fun getString(resource: StringResource): String { val resourceReader = LocalResourceReader.current - val str by rememberState(id, { "" }) { loadString(id, resourceReader) } + val str by rememberState(resource, { "" }) { loadString(resource, resourceReader) } 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. * * @throws IllegalArgumentException If the provided ID is not found in the resource file. */ @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 { - val nameToValue = getParsedStrings(getPathById(STRINGS_XML), resourceReader) - val item = nameToValue[id.stringKey] as? StringItem.Value - ?: error("String ID=`${id.stringKey}` is not found!") +private suspend fun loadString(resource: StringResource, resourceReader: ResourceReader): String { + val path = resource.getPathByEnvironment() + val keyToValue = getParsedStrings(path, resourceReader) + val item = keyToValue[resource.key] as? StringItem.Value + ?: error("String ID=`${resource.key}` is not found!") 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. * @return The formatted string resource. * @@ -87,67 +112,68 @@ private suspend fun loadString(id: ResourceId, resourceReader: ResourceReader): */ @ExperimentalResourceApi @Composable -fun getString(id: ResourceId, vararg formatArgs: Any): String { +fun getString(resource: StringResource, vararg formatArgs: Any): String { val resourceReader = LocalResourceReader.current 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 } /** - * 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. * @return The formatted string resource. * * @throws IllegalArgumentException If the provided ID is not found in the resource file. */ @ExperimentalResourceApi -suspend fun loadString(id: ResourceId, vararg formatArgs: Any): String = loadString( - id, +suspend fun loadString(resource: StringResource, vararg formatArgs: Any): String = loadString( + resource, formatArgs.map { it.toString() }, DefaultResourceReader ) -private suspend fun loadString(id: ResourceId, args: List, resourceReader: ResourceReader): String { - val str = loadString(id, resourceReader) +private suspend fun loadString(resource: StringResource, args: List, resourceReader: ResourceReader): String { + val str = loadString(resource, resourceReader) return SimpleStringFormatRegex.replace(str) { matchResult -> 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. * * @throws IllegalStateException if the string array with the given ID is not found. */ @ExperimentalResourceApi @Composable -fun getStringArray(id: ResourceId): List { +fun getStringArray(resource: StringResource): List { val resourceReader = LocalResourceReader.current - val array by rememberState(id, { emptyList() }) { loadStringArray(id, resourceReader) } + val array by rememberState(resource, { emptyList() }) { loadStringArray(resource, resourceReader) } 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. * * @throws IllegalStateException if the string array with the given ID is not found. */ @ExperimentalResourceApi -suspend fun loadStringArray(id: ResourceId): List = loadStringArray(id, DefaultResourceReader) +suspend fun loadStringArray(resource: StringResource): List = loadStringArray(resource, DefaultResourceReader) -private suspend fun loadStringArray(id: ResourceId, resourceReader: ResourceReader): List { - val nameToValue = getParsedStrings(getPathById(STRINGS_XML), resourceReader) - val item = nameToValue[id.stringKey] as? StringItem.Array - ?: error("String array ID=`${id.stringKey}` is not found!") +private suspend fun loadStringArray(resource: StringResource, resourceReader: ResourceReader): List { + val path = resource.getPathByEnvironment() + val keyToValue = getParsedStrings(path, resourceReader) + val item = keyToValue[resource.key] as? StringItem.Array + ?: error("String array ID=`${resource.key}` is not found!") return item.items } diff --git a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ResourceTest.kt b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ResourceTest.kt index 56c8d5f3af..cd90bd2478 100644 --- a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ResourceTest.kt +++ b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ResourceTest.kt @@ -14,12 +14,12 @@ import kotlin.test.assertNotEquals class ResourceTest { @Test fun testResourceEquals() = runBlockingTest { - assertEquals(getPathById("a"), getPathById("a")) + assertEquals(ImageResource("a"), ImageResource("a")) } @Test fun testResourceNotEquals() = runBlockingTest { - assertNotEquals(getPathById("a"), getPathById("b")) + assertNotEquals(ImageResource("a"), ImageResource("b")) } @Test @@ -28,7 +28,7 @@ class ResourceTest { readResourceBytes("missing.png") } val error = assertFailsWith { - loadString("unknown_id") + loadString(TestStringResource("unknown_id")) } assertEquals("String ID=`unknown_id` is not found!", error.message) } @@ -56,11 +56,11 @@ class ResourceTest { @Test fun testLoadStringResource() = runBlockingTest { - assertEquals("Compose Resources App", loadString("app_name")) + assertEquals("Compose Resources App", loadString(TestStringResource("app_name"))) assertEquals( "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"))) } } diff --git a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt index a8b4ef6c77..929011679a 100644 --- a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt +++ b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt @@ -2,4 +2,10 @@ package org.jetbrains.compose.resources import kotlinx.coroutines.CoroutineScope -expect fun runBlockingTest(block: suspend CoroutineScope.() -> Unit) \ No newline at end of file +expect fun runBlockingTest(block: suspend CoroutineScope.() -> Unit) + +internal fun TestStringResource(key: String) = StringResource( + "STRING:$key", + key, + setOf(ResourceItem(emptySet(), "strings.xml")) +) \ No newline at end of file diff --git a/components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.desktop.kt b/components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.desktop.kt index 5a88b45ccc..aed5d003a4 100644 --- a/components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.desktop.kt +++ b/components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.desktop.kt @@ -25,17 +25,17 @@ class ComposeResourceTest { @Test fun testCountRecompositions() = runComposeUiTest { runBlockingTest { - val imagePathFlow = MutableStateFlow("1.png") + val imagePathFlow = MutableStateFlow(ImageResource("1.png")) val recompositionsCounter = RecompositionsCounter() setContent { - val path by imagePathFlow.collectAsState() - val res = imageResource(path) + val res by imagePathFlow.collectAsState() + val imgRes = imageResource(res) recompositionsCounter.content { - Image(bitmap = res, contentDescription = null) + Image(bitmap = imgRes, contentDescription = null) } } awaitIdle() - imagePathFlow.emit("2.png") + imagePathFlow.emit(ImageResource("2.png")) awaitIdle() assertEquals(2, recompositionsCounter.count) } @@ -45,17 +45,17 @@ class ComposeResourceTest { fun testImageResourceCache() = runComposeUiTest { runBlockingTest { val testResourceReader = TestResourceReader() - val imagePathFlow = MutableStateFlow("1.png") + val imagePathFlow = MutableStateFlow(ImageResource("1.png")) setContent { CompositionLocalProvider(LocalResourceReader provides testResourceReader) { - val path by imagePathFlow.collectAsState() - Image(painterResource(path), null) + val res by imagePathFlow.collectAsState() + Image(painterResource(res), null) } } awaitIdle() - imagePathFlow.emit("2.png") + imagePathFlow.emit(ImageResource("2.png")) awaitIdle() - imagePathFlow.emit("1.png") + imagePathFlow.emit(ImageResource("1.png")) awaitIdle() assertEquals( @@ -69,18 +69,18 @@ class ComposeResourceTest { fun testStringResourceCache() = runComposeUiTest { runBlockingTest { val testResourceReader = TestResourceReader() - val stringIdFlow = MutableStateFlow("app_name") + val stringIdFlow = MutableStateFlow(TestStringResource("app_name")) setContent { CompositionLocalProvider(LocalResourceReader provides testResourceReader) { - val textId by stringIdFlow.collectAsState() - Text(getString(textId)) - Text(getStringArray("str_arr").joinToString()) + val res by stringIdFlow.collectAsState() + Text(getString(res)) + Text(getStringArray(TestStringResource("str_arr")).joinToString()) } } awaitIdle() - stringIdFlow.emit("hello") + stringIdFlow.emit(TestStringResource("hello")) awaitIdle() - stringIdFlow.emit("app_name") + stringIdFlow.emit(TestStringResource("app_name")) awaitIdle() assertEquals( @@ -94,12 +94,12 @@ class ComposeResourceTest { fun testReadStringResource() = runComposeUiTest { runBlockingTest { setContent { - assertEquals("Compose Resources App", getString("app_name")) + assertEquals("Compose Resources App", getString(TestStringResource("app_name"))) assertEquals( "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() } diff --git a/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt b/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt index 0ac5f6f069..02ed4414ac 100644 --- a/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt +++ b/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt @@ -33,11 +33,12 @@ private val defaultEmptyFont by lazy { Font("org.jetbrains.compose.emptyFont", B @ExperimentalResourceApi @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 fontFile by rememberState(id, { defaultEmptyFont }) { - val fontBytes = resourceReader.read(getPathById(id)) - Font(id, fontBytes, weight, style) + val fontFile by rememberState(resource, { defaultEmptyFont }) { + val path = resource.getPathByEnvironment() + val fontBytes = resourceReader.read(path) + Font(path, fontBytes, weight, style) } return fontFile } \ No newline at end of file diff --git a/gradle-plugins/build.gradle.kts b/gradle-plugins/build.gradle.kts index b04154cc33..c8fc305495 100644 --- a/gradle-plugins/build.gradle.kts +++ b/gradle-plugins/build.gradle.kts @@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile plugins { 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.download) apply false } @@ -14,6 +14,7 @@ subprojects { repositories { mavenCentral() + google() mavenLocal() } diff --git a/gradle-plugins/compose/build.gradle.kts b/gradle-plugins/compose/build.gradle.kts index 9cdadd0dac..b0865b2d8c 100644 --- a/gradle-plugins/compose/build.gradle.kts +++ b/gradle-plugins/compose/build.gradle.kts @@ -1,9 +1,10 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import de.undercouch.gradle.tasks.download.Download +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask plugins { alias(libs.plugins.kotlin.jvm) - alias(libs.plugins.publish.plugin.portal) + alias(libs.plugins.publish.plugin) id("java-gradle-plugin") id("maven-publish") 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("composeGradlePluginVersion", BuildProperties.deployVersion(project)) } -tasks.named("compileKotlin") { +tasks.named("compileKotlin", KotlinCompilationTask::class) { dependsOn(buildConfig) + compilerOptions.freeCompilerArgs.add("-opt-in=org.jetbrains.compose.ExperimentalComposeLibrary") } sourceSets.main.configure { java.srcDir(buildConfig.flatMap { it.generatedOutputDir }) @@ -58,11 +60,14 @@ dependencies { compileOnly(kotlin("gradle-plugin-api")) compileOnly(kotlin("gradle-plugin")) compileOnly(kotlin("native-utils")) + compileOnly(libs.plugin.android) + compileOnly(libs.plugin.android.api) testImplementation(gradleTestKit()) testImplementation(kotlin("gradle-plugin-api")) embedded(libs.download.task) + embedded(libs.kotlin.poet) embedded(project(":preview-rpc")) embedded(project(":jdk-version-probe")) } @@ -98,7 +103,7 @@ val gradleTestsPattern = "org.jetbrains.compose.test.tests.integration.*" tasks.registerVerificationTask("checkJar") { dependsOn(jar) jarFile.set(jar.archiveFile) - allowedPackagePrefixes.addAll("org.jetbrains.compose", "kotlinx.serialization") + allowedPackagePrefixes.addAll("org.jetbrains.compose", "kotlinx.serialization", "com.squareup.kotlinpoet") } tasks.test { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt index 47badf9558..a54ee7cb44 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt +++ b/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.configureExperimental 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.mppExt import org.jetbrains.compose.internal.mppExtOrNull import org.jetbrains.compose.internal.service.ConfigurationProblemReporterService import org.jetbrains.compose.internal.service.GradlePropertySnapshotService 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.kotlin.gradle.plugin.KotlinDependencyHandler import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion @@ -64,6 +65,8 @@ abstract class ComposePlugin : Plugin { project.plugins.apply(ComposeCompilerKotlinSupportPlugin::class.java) project.configureNativeCompilerCaching() + project.configureResourceGenerator() + project.afterEvaluate { configureDesktop(project, desktopExtension) project.configureExperimental(composeExtension, experimentalExtension) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt index 3c3094495d..37fee60509 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt +++ b/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_TEAM_ID_PROVIDER = "compose.desktop.mac.notarization.teamID" 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 = providers.valueOrNull(VERBOSE).toBooleanProvider(false) @@ -51,4 +53,10 @@ internal object ComposeProperties { fun checkJdkVendor(providers: ProviderFactory): Provider = providers.valueOrNull(CHECK_JDK_VENDOR).toBooleanProvider(true) + + fun alwaysGenerateResourceAccessors(providers: ProviderFactory): Provider = + providers.valueOrNull(ALWAYS_GENERATE_RESOURCE_ACCESSORS).toBooleanProvider(false) + + fun syncResources(providers: ProviderFactory): Provider = + providers.valueOrNull(SYNC_RESOURCES_PROPERTY).toBooleanProvider(true) } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/IosGradleProperties.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/IosGradleProperties.kt deleted file mode 100644 index 0259e89758..0000000000 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/IosGradleProperties.kt +++ /dev/null @@ -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 = - providers.valueOrNull(SYNC_RESOURCES_PROPERTY).toBooleanProvider(true) -} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidTargetConfiguration.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidTargetConfiguration.kt new file mode 100644 index 0000000000..a9aa1037fa --- /dev/null +++ b/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, + androidFontsDir: Provider, + onlyIfProvider: Provider +) { + 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("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 } + ) + } + ) + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt new file mode 100644 index 0000000000..f8cf6ffc60 --- /dev/null +++ b/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 + + @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>> = 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? { + 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 { + 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() + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesGenerator.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesGenerator.kt new file mode 100644 index 0000000000..10331de455 --- /dev/null +++ b/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 = 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 + ) + } +} + diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt new file mode 100644 index 0000000000..a9063440e8 --- /dev/null +++ b/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, + 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>>, + 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>) = + 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) { + 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() + ) +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/IosTargetResources.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/IosTargetResources.kt similarity index 95% rename from gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/IosTargetResources.kt rename to gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/IosTargetResources.kt index 7259a2a85d..4edad4d9db 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/IosTargetResources.kt +++ b/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. */ -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.SetProperty diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/tasks/SyncComposeResourcesForIosTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/SyncComposeResourcesForIosTask.kt similarity index 94% rename from gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/tasks/SyncComposeResourcesForIosTask.kt rename to gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/SyncComposeResourcesForIosTask.kt index 8d4b7e7264..6df77666cf 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/tasks/SyncComposeResourcesForIosTask.kt +++ b/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. */ -package org.jetbrains.compose.experimental.uikit.tasks +package org.jetbrains.compose.resources.ios import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.FileCollection import org.gradle.api.provider.Provider import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.* -import org.jetbrains.compose.experimental.uikit.internal.resources.determineIosKonanTargetsFromEnv -import org.jetbrains.compose.experimental.uikit.internal.resources.IosTargetResources +import org.jetbrains.compose.experimental.uikit.tasks.AbstractComposeIosTask import org.jetbrains.compose.internal.utils.clearDirs import java.io.File import kotlin.io.path.Path diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/configureSyncIosResources.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/configureSyncIosResources.kt similarity index 94% rename from gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/configureSyncIosResources.kt rename to gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/configureSyncIosResources.kt index 27498091d7..21e47e610a 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/configureSyncIosResources.kt +++ b/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. */ -package org.jetbrains.compose.experimental.uikit.internal.resources +package org.jetbrains.compose.resources.ios import org.gradle.api.Project 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.TaskContainer 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.cocoapodsExt import org.jetbrains.compose.experimental.uikit.internal.utils.withCocoapodsPlugin 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.new 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") } - if (!IosGradleProperties.syncResources(providers).get()) { - reportSyncIsDisabled("'${IosGradleProperties.SYNC_RESOURCES_PROPERTY}' value is 'false'") + if (!ComposeProperties.syncResources(providers).get()) { + reportSyncIsDisabled("'${ComposeProperties.SYNC_RESOURCES_PROPERTY}' value is 'false'") return } @@ -48,7 +47,7 @@ internal fun Project.configureSyncTask(mppExt: KotlinMultiplatformExtension) { } } - with (SyncIosResourcesContext(project, mppExt)) { + with(SyncIosResourcesContext(project, mppExt)) { configureSyncResourcesTasks() configureCocoapodsResourcesAttribute() } @@ -98,7 +97,7 @@ private fun SyncIosResourcesContext.configureCocoapodsResourcesAttribute() { error(""" |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; - | * 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()) } cocoapodsExt.framework { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/determineIosKonanTargetsFromEnv.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/determineIosKonanTargetsFromEnv.kt similarity index 95% rename from gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/determineIosKonanTargetsFromEnv.kt rename to gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ios/determineIosKonanTargetsFromEnv.kt index c1df81b773..64fd097239 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/resources/determineIosKonanTargetsFromEnv.kt +++ b/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. */ -package org.jetbrains.compose.experimental.uikit.internal.resources +package org.jetbrains.compose.resources.ios import org.jetbrains.kotlin.konan.target.KonanTarget diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt new file mode 100644 index 0000000000..2a47875137 --- /dev/null +++ b/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") + } + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProject.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProject.kt index a0cf78f314..97648b916e 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProject.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProject.kt @@ -15,6 +15,7 @@ import java.util.Properties data class TestEnvironment( val workingDir: File, val kotlinVersion: String = TestKotlinVersions.Default, + val agpVersion: String = "7.3.1", val composeGradlePluginVersion: String = TestProperties.composeGradlePluginVersion, val mokoResourcesPluginVersion: String = "0.23.0", val composeCompilerPlugin: String? = null, @@ -26,6 +27,7 @@ data class TestEnvironment( private val placeholders = linkedMapOf( "COMPOSE_GRADLE_PLUGIN_VERSION_PLACEHOLDER" to composeGradlePluginVersion, "KOTLIN_VERSION_PLACEHOLDER" to kotlinVersion, + "AGP_VERSION_PLACEHOLDER" to agpVersion, "COMPOSE_COMPILER_PLUGIN_PLACEHOLDER" to composeCompilerPlugin, "COMPOSE_COMPILER_PLUGIN_ARGS_PLACEHOLDER" to composeCompilerArgs, "MOKO_RESOURCES_PLUGIN_VERSION_PLACEHOLDER" to mokoResourcesPluginVersion, diff --git a/gradle-plugins/compose/src/test/test-projects/application/mpp/settings.gradle b/gradle-plugins/compose/src/test/test-projects/application/mpp/settings.gradle index a7586cae34..919c627862 100644 --- a/gradle-plugins/compose/src/test/test-projects/application/mpp/settings.gradle +++ b/gradle-plugins/compose/src/test/test-projects/application/mpp/settings.gradle @@ -2,7 +2,7 @@ pluginManagement { plugins { id 'org.jetbrains.kotlin.multiplatform' version 'KOTLIN_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 { mavenLocal() diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/build.gradle.kts b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/build.gradle.kts new file mode 100644 index 0000000000..622b7a02ed --- /dev/null +++ b/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 + } +} diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt new file mode 100644 index 0000000000..33411f4ba9 --- /dev/null +++ b/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"), + ) + ) + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/gradle.properties b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/gradle.properties new file mode 100644 index 0000000000..2d8d1e4dd1 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/gradle.properties @@ -0,0 +1 @@ +android.useAndroidX=true \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/settings.gradle.kts b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/settings.gradle.kts new file mode 100644 index 0000000000..912b547cf6 --- /dev/null +++ b/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() + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/kotlin/App.kt b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/kotlin/App.kt new file mode 100644 index 0000000000..f83ac5962e --- /dev/null +++ b/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)) + } +} diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/fonts/emptyFont.otf b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/fonts/emptyFont.otf new file mode 100644 index 0000000000..883bb179cb Binary files /dev/null and b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/fonts/emptyFont.otf differ diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/ignored/ignored_3.txt b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/ignored/ignored_3.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/ignored_1.txt b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/ignored_1.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q1-q2/vector.xml b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q1-q2/vector.xml new file mode 100644 index 0000000000..d7bf7955f4 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q1-q2/vector.xml @@ -0,0 +1,36 @@ + + + + + + + + diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q1/vector.xml b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q1/vector.xml new file mode 100644 index 0000000000..d7bf7955f4 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q1/vector.xml @@ -0,0 +1,36 @@ + + + + + + + + diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q2/vector.xml b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q2/vector.xml new file mode 100644 index 0000000000..d7bf7955f4 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images-q2/vector.xml @@ -0,0 +1,36 @@ + + + + + + + + diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images/vector.xml b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images/vector.xml new file mode 100644 index 0000000000..d7bf7955f4 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images/vector.xml @@ -0,0 +1,36 @@ + + + + + + + + diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images/vector_2.xml b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images/vector_2.xml new file mode 100644 index 0000000000..d7bf7955f4 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/images/vector_2.xml @@ -0,0 +1,36 @@ + + + + + + + + diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/values/strings.xml b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/values/strings.xml new file mode 100644 index 0000000000..bdfed65b61 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/composeRes/values/strings.xml @@ -0,0 +1,13 @@ + + Compose Resources App + 😊 Hello world! + Lorem ipsum dolor sit amet, + consectetur adipiscing elit. + Donec eget turpis ac sem ultricies consequat. + Hello, %1$s! You have %2$d new messages. + + item 1 + item 2 + item 3 + + diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/ignored_2.txt b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/resources/ignored_2.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gradle-plugins/gradle.properties b/gradle-plugins/gradle.properties index 50180a4398..64c141928c 100644 --- a/gradle-plugins/gradle.properties +++ b/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 # __SUPPORTED_GRADLE_VERSIONS__ # 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, # unless overridden by COMPOSE_GRADLE_PLUGIN_VERSION env var. diff --git a/gradle-plugins/gradle/libs.versions.toml b/gradle-plugins/gradle/libs.versions.toml index 4281a1056a..1b70767c49 100644 --- a/gradle-plugins/gradle/libs.versions.toml +++ b/gradle-plugins/gradle/libs.versions.toml @@ -1,12 +1,19 @@ [versions] kotlin = "1.9.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] 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] 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" } -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" }