diff --git a/components/AnimatedImage/demo/src/jvmMain/kotlin/org/jetbrains/compose/animatedimage/demo/Main.kt b/components/AnimatedImage/demo/src/jvmMain/kotlin/org/jetbrains/compose/animatedimage/demo/Main.kt index 34d18c9308..3e4a5f32fe 100644 --- a/components/AnimatedImage/demo/src/jvmMain/kotlin/org/jetbrains/compose/animatedimage/demo/Main.kt +++ b/components/AnimatedImage/demo/src/jvmMain/kotlin/org/jetbrains/compose/animatedimage/demo/Main.kt @@ -5,30 +5,58 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.size import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.unit.dp import androidx.compose.ui.window.singleWindowApplication +import org.jetbrains.compose.animatedimage.AnimatedImage import org.jetbrains.compose.animatedimage.Blank import org.jetbrains.compose.animatedimage.animate import org.jetbrains.compose.animatedimage.loadAnimatedImage -import org.jetbrains.compose.resources.LoadState -import org.jetbrains.compose.resources.load -import org.jetbrains.compose.resources.loadOrNull private val url = "https://raw.githubusercontent.com/JetBrains/skija/ccf303ebcf926e5ef000fc42d1a6b5b7f1e0b2b5/examples/scenes/images/codecs/animated.gif" +private sealed interface LoadState { + class Loading : LoadState + data class Success(val data: T) : LoadState + data class Error(val error: Exception) : LoadState +} + +@Composable +private fun loadOrNull(action: suspend () -> T?): T? { + val scope = rememberCoroutineScope() + var result: T? by remember { mutableStateOf(null) } + LaunchedEffect(Unit) { + result = action() + } + return result +} + fun main() = singleWindowApplication { Column { - // Load an image async - // use "load { loadResourceAnimatedImage(url) }" for resources - when (val animatedImage = load { loadAnimatedImage(url) }) { + var state: LoadState = LoadState.Loading() + LaunchedEffect(url) { + state = try { + LoadState.Success(loadAnimatedImage(url)) + } catch (e: Exception) { + LoadState.Error(e) + } + } + + when (val animatedImage = state) { is LoadState.Success -> Image( - bitmap = animatedImage.value.animate(), + bitmap = animatedImage.data.animate(), contentDescription = null, ) + is LoadState.Loading -> CircularProgressIndicator() is LoadState.Error -> Text("Error!") } diff --git a/components/AnimatedImage/library/build.gradle.kts b/components/AnimatedImage/library/build.gradle.kts index a9e34b5cec..be3889bb81 100644 --- a/components/AnimatedImage/library/build.gradle.kts +++ b/components/AnimatedImage/library/build.gradle.kts @@ -16,7 +16,6 @@ kotlin { dependencies { api(compose.runtime) api(compose.foundation) - api(project(":resources:library")) } } } diff --git a/components/gradle.properties b/components/gradle.properties index f8c7b6e4ad..69b07d65ad 100644 --- a/components/gradle.properties +++ b/components/gradle.properties @@ -23,4 +23,5 @@ kotlin.code.style=official kotlin.js.compiler=ir kotlin.js.webpack.major.version=4 kotlin.native.useEmbeddableCompilerJar=true -kotlin.native.binary.memoryModel=experimental \ No newline at end of file +kotlin.native.binary.memoryModel=experimental +xcodeproj=./resources/demo/iosApp diff --git a/components/resources/demo/shared/build.gradle.kts b/components/resources/demo/shared/build.gradle.kts index 2027ecd3f5..74b2417b25 100644 --- a/components/resources/demo/shared/build.gradle.kts +++ b/components/resources/demo/shared/build.gradle.kts @@ -48,12 +48,16 @@ kotlin { } sourceSets { + all { + languageSettings { + optIn("org.jetbrains.compose.resources.ExperimentalResourceApi") + } + } val commonMain by getting { dependencies { - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material) implementation(compose.runtime) + implementation(compose.material3) + implementation(compose.materialIconsExtended) implementation(project(":resources:library")) } } diff --git a/components/resources/demo/shared/src/androidMain/assets/font_awesome.otf b/components/resources/demo/shared/src/androidMain/assets/font_awesome.otf new file mode 100644 index 0000000000..401ec0f36e Binary files /dev/null and b/components/resources/demo/shared/src/androidMain/assets/font_awesome.otf differ 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 new file mode 100644 index 0000000000..2d6aab445f --- /dev/null +++ b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt @@ -0,0 +1,63 @@ +package org.jetbrains.compose.resources.demo.shared + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.readResourceBytes + +@Composable +fun FileRes(paddingValues: PaddingValues) { + Column( + modifier = Modifier.padding(paddingValues) + ) { + Text( + modifier = Modifier.padding(16.dp), + text = "File: 'images/droid_icon.xml'", + style = MaterialTheme.typography.titleLarge + ) + OutlinedCard( + modifier = Modifier.padding(horizontal = 16.dp), + shape = RoundedCornerShape(4.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer) + ) { + var bytes by remember { mutableStateOf(ByteArray(0)) } + LaunchedEffect(Unit) { + bytes = readResourceBytes("images/droid_icon.xml") + } + Text( + modifier = Modifier.padding(8.dp).height(200.dp).verticalScroll(rememberScrollState()), + text = bytes.decodeToString(), + color = MaterialTheme.colorScheme.onPrimaryContainer, + softWrap = false + ) + } + Text( + modifier = Modifier.padding(16.dp), + text = """ + var bytes by remember { + mutableStateOf(ByteArray(0)) + } + LaunchedEffect(Unit) { + bytes = readBytes("images/droid_icon.xml") + } + Text(bytes.decodeToString()) + """.trimIndent() + ) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..55cb969388 --- /dev/null +++ b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FontRes.kt @@ -0,0 +1,54 @@ +package org.jetbrains.compose.resources.demo.shared + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.Font + +@Composable +fun FontRes(paddingValues: PaddingValues) { + Column( + modifier = Modifier.padding(paddingValues) + ) { + OutlinedCard( + modifier = Modifier.padding(16.dp).fillMaxWidth(), + shape = RoundedCornerShape(4.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer) + ) { + Text( + modifier = Modifier.padding(8.dp), + text = """ + val fontAwesome = FontFamily(Font("font_awesome.otf")) + val symbols = arrayOf(0xf1ba, 0xf238, 0xf21a, 0xf1bb, 0xf1b8, 0xf09b, 0xf269, 0xf1d0, 0xf15a, 0xf293, 0xf1c6) + Text( + modifier = Modifier.padding(16.dp), + fontFamily = fontAwesome, + style = MaterialTheme.typography.headlineLarge, + text = symbols.joinToString(" ") { it.toChar().toString() } + ) + """.trimIndent(), + color = MaterialTheme.colorScheme.onPrimaryContainer, + softWrap = false + ) + } + + val fontAwesome = FontFamily(Font("font_awesome.otf")) + val symbols = arrayOf(0xf1ba, 0xf238, 0xf21a, 0xf1bb, 0xf1b8, 0xf09b, 0xf269, 0xf1d0, 0xf15a, 0xf293, 0xf1c6) + Text( + modifier = Modifier.padding(16.dp), + fontFamily = fontAwesome, + style = MaterialTheme.typography.headlineLarge, + text = symbols.joinToString(" ") { it.toChar().toString() } + ) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..73c81ed419 --- /dev/null +++ b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/ImagesRes.kt @@ -0,0 +1,176 @@ +package org.jetbrains.compose.resources.demo.shared + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.imageResource +import org.jetbrains.compose.resources.vectorResource +import org.jetbrains.compose.resources.painterResource + +@Composable +fun ImagesRes(contentPadding: PaddingValues) { + Column( + modifier = Modifier.padding(contentPadding).verticalScroll(rememberScrollState()), + ) { + OutlinedCard(modifier = Modifier.padding(8.dp)) { + Column( + modifier = Modifier.padding(16.dp).fillMaxWidth().fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + modifier = Modifier.size(100.dp), + painter = painterResource("images/compose.png"), + contentDescription = null + ) + Text( + """ + Image( + painter = painterResource("images/compose.png") + ) + """.trimIndent() + ) + } + } + OutlinedCard(modifier = Modifier.padding(8.dp)) { + Column( + modifier = Modifier.padding(16.dp).fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + modifier = Modifier.size(100.dp), + painter = painterResource("images/insta_icon.xml"), + contentDescription = null + ) + Text( + """ + Image( + painter = painterResource("images/insta_icon.xml") + ) + """.trimIndent() + ) + } + } + OutlinedCard(modifier = Modifier.padding(8.dp)) { + Column( + modifier = Modifier.padding(16.dp).fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + modifier = Modifier.size(140.dp), + bitmap = imageResource("images/land.webp"), + contentDescription = null + ) + Text( + """ + Image( + bitmap = imageResource("images/land.webp") + ) + """.trimIndent() + ) + } + } + OutlinedCard(modifier = Modifier.padding(8.dp)) { + Column( + modifier = Modifier.padding(16.dp).fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + modifier = Modifier.size(100.dp), + imageVector = vectorResource("images/droid_icon.xml"), + contentDescription = null + ) + Text( + """ + Image( + imageVector = vectorResource("images/droid_icon.xml") + ) + """.trimIndent() + ) + } + } + OutlinedCard(modifier = Modifier.padding(8.dp)) { + Column( + modifier = Modifier.padding(16.dp).fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + modifier = Modifier.size(100.dp), + painter = painterResource("images/compose.png"), + contentDescription = null + ) + Text( + """ + Icon( + painter = painterResource("images/compose.png") + ) + """.trimIndent() + ) + } + } + OutlinedCard(modifier = Modifier.padding(8.dp)) { + Column( + modifier = Modifier.padding(16.dp).fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + modifier = Modifier.size(100.dp), + painter = painterResource("images/insta_icon.xml"), + contentDescription = null + ) + Text( + """ + Icon( + painter = painterResource("images/insta_icon.xml") + ) + """.trimIndent() + ) + } + } + OutlinedCard(modifier = Modifier.padding(8.dp)) { + Column( + modifier = Modifier.padding(16.dp).fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + modifier = Modifier.size(140.dp), + bitmap = imageResource("images/land.webp"), + contentDescription = null + ) + Text( + """ + Icon( + bitmap = imageResource("images/land.webp") + ) + """.trimIndent() + ) + } + } + OutlinedCard(modifier = Modifier.padding(8.dp)) { + Column( + modifier = Modifier.padding(16.dp).fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + modifier = Modifier.size(100.dp), + imageVector = vectorResource("images/droid_icon.xml"), + contentDescription = null + ) + Text( + """ + Icon( + imageVector = vectorResource("images/droid_icon.xml") + ) + """.trimIndent() + ) + } + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..7ba300ba6f --- /dev/null +++ b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt @@ -0,0 +1,115 @@ +package org.jetbrains.compose.resources.demo.shared + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.getString +import org.jetbrains.compose.resources.getStringArray +import org.jetbrains.compose.resources.readResourceBytes + +@Composable +fun StringRes(paddingValues: PaddingValues) { + Column( + modifier = Modifier.padding(paddingValues).verticalScroll(rememberScrollState()) + ) { + Text( + modifier = Modifier.padding(16.dp), + text = "strings.xml", + style = MaterialTheme.typography.titleLarge + ) + OutlinedCard( + modifier = Modifier.padding(horizontal = 16.dp).fillMaxWidth(), + shape = RoundedCornerShape(4.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer) + ) { + var bytes by remember { mutableStateOf(ByteArray(0)) } + LaunchedEffect(Unit) { + bytes = readResourceBytes("strings.xml") + } + Text( + modifier = Modifier.padding(8.dp), + text = bytes.decodeToString(), + color = MaterialTheme.colorScheme.onPrimaryContainer, + softWrap = false + ) + } + OutlinedTextField( + modifier = Modifier.padding(16.dp).fillMaxWidth(), + value = getString("app_name"), + onValueChange = {}, + label = { Text("Text(getString(\"app_name\"))") }, + enabled = false, + colors = TextFieldDefaults.colors( + disabledTextColor = MaterialTheme.colorScheme.onSurface, + disabledContainerColor = MaterialTheme.colorScheme.surface, + disabledLabelColor = MaterialTheme.colorScheme.onSurface, + ) + ) + OutlinedTextField( + modifier = Modifier.padding(16.dp).fillMaxWidth(), + value = getString("hello"), + onValueChange = {}, + label = { Text("Text(getString(\"hello\"))") }, + enabled = false, + colors = TextFieldDefaults.colors( + disabledTextColor = MaterialTheme.colorScheme.onSurface, + disabledContainerColor = MaterialTheme.colorScheme.surface, + disabledLabelColor = MaterialTheme.colorScheme.onSurface, + ) + ) + OutlinedTextField( + modifier = Modifier.padding(16.dp).fillMaxWidth(), + value = getString("multi_line"), + onValueChange = {}, + label = { Text("Text(getString(\"multi_line\"))") }, + enabled = false, + colors = TextFieldDefaults.colors( + disabledTextColor = MaterialTheme.colorScheme.onSurface, + disabledContainerColor = MaterialTheme.colorScheme.surface, + disabledLabelColor = MaterialTheme.colorScheme.onSurface, + ) + ) + OutlinedTextField( + modifier = Modifier.padding(16.dp).fillMaxWidth(), + value = getString("str_template", "User_name", 100), + onValueChange = {}, + label = { Text("Text(getString(\"str_template\", \"User_name\", 100))") }, + enabled = false, + colors = TextFieldDefaults.colors( + disabledTextColor = MaterialTheme.colorScheme.onSurface, + disabledContainerColor = MaterialTheme.colorScheme.surface, + disabledLabelColor = MaterialTheme.colorScheme.onSurface, + ) + ) + OutlinedTextField( + modifier = Modifier.padding(16.dp).fillMaxWidth(), + value = getStringArray("str_arr").toString(), + onValueChange = {}, + label = { Text("Text(getStringArray(\"str_arr\").toString())") }, + enabled = false, + colors = TextFieldDefaults.colors( + disabledTextColor = MaterialTheme.colorScheme.onSurface, + disabledContainerColor = MaterialTheme.colorScheme.surface, + disabledLabelColor = MaterialTheme.colorScheme.onSurface, + ) + ) + } +} \ No newline at end of file diff --git a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/UseResources.kt b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/UseResources.kt index b1fa10c534..9f390a082d 100644 --- a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/UseResources.kt +++ b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/UseResources.kt @@ -1,37 +1,65 @@ package org.jetbrains.compose.resources.demo.shared -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* -import androidx.compose.material.Icon -import androidx.compose.material.Text +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Abc +import androidx.compose.material.icons.filled.Attachment +import androidx.compose.material.icons.filled.Image +import androidx.compose.material.icons.filled.TextFields +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.dp -import org.jetbrains.compose.resources.* -@OptIn(ExperimentalResourceApi::class) +enum class Screens(val content: @Composable (contentPadding: PaddingValues) -> Unit) { + Images({ ImagesRes(it) }), + Strings({ StringRes(it) }), + Font({ FontRes(it) }), + File({ FileRes(it) }), +} + @Composable internal fun UseResources() { - Column { - Text("Hello, resources") - Image( - bitmap = resource("dir/img.png").rememberImageBitmap().orEmpty(), - contentDescription = null, - ) - Image( - bitmap = resource("img.webp").rememberImageBitmap().orEmpty(), - contentDescription = null, - ) - Icon( - imageVector = resource("vector.xml").rememberImageVector(LocalDensity.current).orEmpty(), - modifier = Modifier.size(150.dp), - contentDescription = null - ) - Icon( - painter = painterResource("dir/vector.xml"), - modifier = Modifier.size(150.dp), - contentDescription = null - ) - } + var screen by remember { mutableStateOf(Screens.Images) } + + Scaffold( + modifier = Modifier.fillMaxSize(), + content = { screen.content(it) }, + bottomBar = { + NavigationBar { + NavigationBarItem( + selected = screen == Screens.Images, + onClick = { screen = Screens.Images }, + icon = { Icon(imageVector = Icons.Default.Image, contentDescription = null) }, + label = { Text("Images") } + ) + NavigationBarItem( + selected = screen == Screens.Strings, + onClick = { screen = Screens.Strings }, + icon = { Icon(imageVector = Icons.Default.Abc, contentDescription = null) }, + label = { Text("Strings") } + ) + NavigationBarItem( + selected = screen == Screens.Font, + onClick = { screen = Screens.Font }, + icon = { Icon(imageVector = Icons.Default.TextFields, contentDescription = null) }, + label = { Text("Fonts") } + ) + NavigationBarItem( + selected = screen == Screens.File, + onClick = { screen = Screens.File }, + icon = { Icon(imageVector = Icons.Default.Attachment, contentDescription = null) }, + label = { Text("Files") } + ) + } + } + ) } diff --git a/components/resources/demo/shared/src/commonMain/resources/font_awesome.otf b/components/resources/demo/shared/src/commonMain/resources/font_awesome.otf new file mode 100644 index 0000000000..401ec0f36e Binary files /dev/null and b/components/resources/demo/shared/src/commonMain/resources/font_awesome.otf differ diff --git a/components/resources/demo/shared/src/commonMain/resources/dir/img.png b/components/resources/demo/shared/src/commonMain/resources/images/compose.png similarity index 100% rename from components/resources/demo/shared/src/commonMain/resources/dir/img.png rename to components/resources/demo/shared/src/commonMain/resources/images/compose.png diff --git a/components/resources/demo/shared/src/commonMain/resources/vector.xml b/components/resources/demo/shared/src/commonMain/resources/images/droid_icon.xml similarity index 100% rename from components/resources/demo/shared/src/commonMain/resources/vector.xml rename to components/resources/demo/shared/src/commonMain/resources/images/droid_icon.xml diff --git a/components/resources/demo/shared/src/commonMain/resources/dir/vector.xml b/components/resources/demo/shared/src/commonMain/resources/images/insta_icon.xml similarity index 100% rename from components/resources/demo/shared/src/commonMain/resources/dir/vector.xml rename to components/resources/demo/shared/src/commonMain/resources/images/insta_icon.xml diff --git a/components/resources/demo/shared/src/commonMain/resources/img.webp b/components/resources/demo/shared/src/commonMain/resources/images/land.webp similarity index 100% rename from components/resources/demo/shared/src/commonMain/resources/img.webp rename to components/resources/demo/shared/src/commonMain/resources/images/land.webp diff --git a/components/resources/demo/shared/src/commonMain/resources/strings.xml b/components/resources/demo/shared/src/commonMain/resources/strings.xml new file mode 100644 index 0000000000..bdfed65b61 --- /dev/null +++ b/components/resources/demo/shared/src/commonMain/resources/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/components/resources/demo/shared/src/desktopMain/kotlin/org/jetbrains/compose/resources/demo/shared/main.desktop.kt b/components/resources/demo/shared/src/desktopMain/kotlin/org/jetbrains/compose/resources/demo/shared/main.desktop.kt index d1997f205b..564bd858c8 100644 --- a/components/resources/demo/shared/src/desktopMain/kotlin/org/jetbrains/compose/resources/demo/shared/main.desktop.kt +++ b/components/resources/demo/shared/src/desktopMain/kotlin/org/jetbrains/compose/resources/demo/shared/main.desktop.kt @@ -6,6 +6,7 @@ package org.jetbrains.compose.resources.demo.shared import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable @Composable @@ -15,6 +16,18 @@ fun MainView() { @Preview @Composable -fun Preview() { +fun MainViewPreview() { MainView() } + +@Preview +@Composable +fun ImagesResPreview() { + ImagesRes(PaddingValues()) +} + +@Preview +@Composable +fun FileResPreview() { + FileRes(PaddingValues()) +} diff --git a/components/resources/demo/shared/src/iosMain/kotlin/main.ios.kt b/components/resources/demo/shared/src/iosMain/kotlin/main.ios.kt index b3f0107a7d..8f5c2b8ba8 100644 --- a/components/resources/demo/shared/src/iosMain/kotlin/main.ios.kt +++ b/components/resources/demo/shared/src/iosMain/kotlin/main.ios.kt @@ -3,21 +3,9 @@ * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. */ -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.height -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.compose.ui.window.ComposeUIViewController import org.jetbrains.compose.resources.demo.shared.UseResources -fun MainViewController() = - ComposeUIViewController { - Column { - Box( - modifier = Modifier - .height(100.dp) - ) - UseResources() - } - } +fun MainViewController() = ComposeUIViewController { + UseResources() +} diff --git a/components/resources/demo/shared/src/jsMain/kotlin/main.js.kt b/components/resources/demo/shared/src/jsMain/kotlin/main.js.kt index 3377e36df4..6a065b6f07 100644 --- a/components/resources/demo/shared/src/jsMain/kotlin/main.js.kt +++ b/components/resources/demo/shared/src/jsMain/kotlin/main.js.kt @@ -3,39 +3,23 @@ * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. */ -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Window +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.CanvasBasedWindow import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.configureWebResources import org.jetbrains.compose.resources.demo.shared.UseResources -import org.jetbrains.compose.resources.urlResource import org.jetbrains.skiko.wasm.onWasmReady +@OptIn(ExperimentalComposeUiApi::class, ExperimentalResourceApi::class) fun main() { - - @OptIn(ExperimentalResourceApi::class) configureWebResources { // Not necessary - It's the same as the default. We add it here just to present this feature. - setResourceFactory { urlResource("./$it") } + resourcePathMapping { path -> "./$path" } } onWasmReady { - Window("Resources demo") { - MainView() + CanvasBasedWindow("Resources demo") { + UseResources() } } } - -@Composable -fun MainView() { - Column(modifier = Modifier.fillMaxSize()) { - Spacer(modifier = Modifier.height(24.dp)) - UseResources() - } -} diff --git a/components/resources/demo/shared/src/jsMain/resources/index.html b/components/resources/demo/shared/src/jsMain/resources/index.html index 971817e6a8..cb395136d4 100644 --- a/components/resources/demo/shared/src/jsMain/resources/index.html +++ b/components/resources/demo/shared/src/jsMain/resources/index.html @@ -2,15 +2,13 @@ - compose multiplatform web demo - - + compose resources web demo + -

compose multiplatform web demo

-
- -
- +
+ +
+ diff --git a/components/resources/demo/shared/src/jsMain/resources/styles.css b/components/resources/demo/shared/src/jsMain/resources/styles.css deleted file mode 100644 index e5b3293a7a..0000000000 --- a/components/resources/demo/shared/src/jsMain/resources/styles.css +++ /dev/null @@ -1,8 +0,0 @@ -#root { - width: 100%; - height: 100vh; -} - -#root > .compose-web-column > div { - position: relative; -} \ No newline at end of file diff --git a/components/resources/library/build.gradle.kts b/components/resources/library/build.gradle.kts index fe0a9c1d6c..8fa273110e 100644 --- a/components/resources/library/build.gradle.kts +++ b/components/resources/library/build.gradle.kts @@ -39,13 +39,23 @@ kotlin { languageSettings { optIn("kotlin.RequiresOptIn") optIn("kotlinx.cinterop.ExperimentalForeignApi") + optIn("kotlin.experimental.ExperimentalNativeApi") } } + // common + // ┌────┴────┐ + // skiko blocking + // │ ┌─────┴────────┐ + // ┌───┴───┬──│────────┐ │ + // │ native │ jvmAndAndroid + // │ ┌───┴───┐ │ ┌───┴───┐ + // js ios macos desktop android + val commonMain by getting { dependencies { - implementation("org.jetbrains.compose.runtime:runtime:$composeVersion") - implementation("org.jetbrains.compose.foundation:foundation:$composeVersion") + implementation(compose.runtime) + implementation(compose.foundation) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") } } @@ -55,10 +65,10 @@ kotlin { implementation(kotlin("test")) } } - val commonButJSMain by creating { + val blockingMain by creating { dependsOn(commonMain) } - val commonButJSTest by creating { + val blockingTest by creating { dependsOn(commonTest) } val skikoMain by creating { @@ -68,20 +78,21 @@ kotlin { dependsOn(commonTest) } val jvmAndAndroidMain by creating { - dependsOn(commonMain) + dependsOn(blockingMain) + dependencies { + implementation(compose.material3) + } } val jvmAndAndroidTest by creating { - dependsOn(commonTest) + dependsOn(blockingTest) } val desktopMain by getting { dependsOn(skikoMain) dependsOn(jvmAndAndroidMain) - dependsOn(commonButJSMain) } val desktopTest by getting { dependsOn(skikoTest) dependsOn(jvmAndAndroidTest) - dependsOn(commonButJSTest) dependencies { implementation(compose.desktop.currentOs) implementation("org.jetbrains.compose.ui:ui-test-junit4:$composeVersion") @@ -90,20 +101,26 @@ kotlin { } val androidMain by getting { dependsOn(jvmAndAndroidMain) - dependsOn(commonButJSMain) } val androidInstrumentedTest by getting { - dependsOn(commonTest) dependsOn(jvmAndAndroidTest) - dependsOn(commonButJSTest) + dependencies { + implementation("androidx.test:core:1.5.0") + implementation("androidx.compose.ui:ui-test-manifest:1.5.4") + implementation("androidx.compose.ui:ui-test:1.5.4") + implementation("androidx.compose.ui:ui-test-junit4:1.5.4") + } + } + val androidUnitTest by getting { + dependsOn(jvmAndAndroidTest) } - val iosMain by getting { + val nativeMain by getting { dependsOn(skikoMain) - dependsOn(commonButJSMain) + dependsOn(blockingMain) } - val iosTest by getting { + val nativeTest by getting { dependsOn(skikoTest) - dependsOn(commonButJSTest) + dependsOn(blockingTest) } val jsMain by getting { dependsOn(skikoMain) @@ -111,14 +128,6 @@ kotlin { val jsTest by getting { dependsOn(skikoTest) } - val macosMain by getting { - dependsOn(skikoMain) - dependsOn(commonButJSMain) - } - val macosTest by getting { - dependsOn(skikoTest) - dependsOn(commonButJSTest) - } } } @@ -145,12 +154,13 @@ android { } } } - dependencies { - //Android integration tests - testImplementation("androidx.test:core:1.5.0") - androidTestImplementation("androidx.compose.ui:ui-test-manifest:1.5.3") - androidTestImplementation("androidx.compose.ui:ui-test:1.5.3") - androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.5.3") + sourceSets { + val commonTestResources = "src/commonTest/resources" + named("androidTest") { + resources.srcDir(commonTestResources) + assets.srcDir("src/androidInstrumentedTest/assets") + } + named("test") { resources.srcDir(commonTestResources) } } } diff --git a/components/resources/library/src/androidInstrumentedTest/assets/font_awesome.otf b/components/resources/library/src/androidInstrumentedTest/assets/font_awesome.otf new file mode 100644 index 0000000000..401ec0f36e Binary files /dev/null and b/components/resources/library/src/androidInstrumentedTest/assets/font_awesome.otf 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 new file mode 100644 index 0000000000..5a88b45ccc --- /dev/null +++ b/components/resources/library/src/androidInstrumentedTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.android.kt @@ -0,0 +1,107 @@ +package org.jetbrains.compose.resources + +import androidx.compose.foundation.Image +import androidx.compose.material3.Text +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.runComposeUiTest +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalResourceApi::class, ExperimentalTestApi::class) +class ComposeResourceTest { + + @Before + fun dropCaches() { + dropStringsCache() + dropImageCache() + } + + @Test + fun testCountRecompositions() = runComposeUiTest { + runBlockingTest { + val imagePathFlow = MutableStateFlow("1.png") + val recompositionsCounter = RecompositionsCounter() + setContent { + val path by imagePathFlow.collectAsState() + val res = imageResource(path) + recompositionsCounter.content { + Image(bitmap = res, contentDescription = null) + } + } + awaitIdle() + imagePathFlow.emit("2.png") + awaitIdle() + assertEquals(2, recompositionsCounter.count) + } + } + + @Test + fun testImageResourceCache() = runComposeUiTest { + runBlockingTest { + val testResourceReader = TestResourceReader() + val imagePathFlow = MutableStateFlow("1.png") + setContent { + CompositionLocalProvider(LocalResourceReader provides testResourceReader) { + val path by imagePathFlow.collectAsState() + Image(painterResource(path), null) + } + } + awaitIdle() + imagePathFlow.emit("2.png") + awaitIdle() + imagePathFlow.emit("1.png") + awaitIdle() + + assertEquals( + expected = listOf("1.png", "2.png"), //no second read of 1.png + actual = testResourceReader.readPaths + ) + } + } + + @Test + fun testStringResourceCache() = runComposeUiTest { + runBlockingTest { + val testResourceReader = TestResourceReader() + val stringIdFlow = MutableStateFlow("app_name") + setContent { + CompositionLocalProvider(LocalResourceReader provides testResourceReader) { + val textId by stringIdFlow.collectAsState() + Text(getString(textId)) + Text(getStringArray("str_arr").joinToString()) + } + } + awaitIdle() + stringIdFlow.emit("hello") + awaitIdle() + stringIdFlow.emit("app_name") + awaitIdle() + + assertEquals( + expected = listOf("strings.xml"), //just one string.xml read + actual = testResourceReader.readPaths + ) + } + } + + @Test + fun testReadStringResource() = runComposeUiTest { + runBlockingTest { + setContent { + assertEquals("Compose Resources App", getString("app_name")) + assertEquals( + "Hello, test-name! You have 42 new messages.", + getString("str_template", "test-name", 42) + ) + assertEquals(listOf("item 1", "item 2", "item 3"), getStringArray("str_arr")) + } + awaitIdle() + } + } +} diff --git a/components/resources/library/src/androidInstrumentedTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt b/components/resources/library/src/androidInstrumentedTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt deleted file mode 100644 index 2803875019..0000000000 --- a/components/resources/library/src/androidInstrumentedTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt +++ /dev/null @@ -1,61 +0,0 @@ -package org.jetbrains.compose.resources - -import androidx.compose.foundation.Image -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.test.junit4.createComposeRule -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertEquals -import org.junit.Rule -import org.junit.Test - -@OptIn(ExperimentalResourceApi::class, ExperimentalCoroutinesApi::class) -class ComposeResourceTest { - - @get:Rule - val rule = createComposeRule() - - @Test - fun testMissingResource() = runTest (UnconfinedTestDispatcher()) { - var recompositionCount = 0 - rule.setContent { - CountRecompositions(resource("missing.png").rememberImageBitmap().orEmpty()) { - recompositionCount++ - } - } - rule.awaitIdle() - assertEquals(2, recompositionCount) - } - - @Test - fun testCountRecompositions() = runTest (UnconfinedTestDispatcher()) { - val mutableStateFlow = MutableStateFlow(true) - var recompositionCount = 0 - rule.setContent { - val state: Boolean by mutableStateFlow.collectAsState(true) - val resource = resource(if (state) "1.png" else "2.png") - CountRecompositions(resource.rememberImageBitmap().orEmpty()) { - recompositionCount++ - } - } - rule.awaitIdle() - mutableStateFlow.value = false - rule.awaitIdle() - assertEquals(4, recompositionCount) - } - -} - -@Composable -private fun CountRecompositions(imageBitmap: ImageBitmap?, onRecomposition: () -> Unit) { - onRecomposition() - if (imageBitmap != null) { - Image(bitmap = imageBitmap, contentDescription = null) - } -} diff --git a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ComposableResource.android.kt b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ComposableResource.android.kt deleted file mode 100644 index 01c312d1db..0000000000 --- a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ComposableResource.android.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package org.jetbrains.compose.resources - -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap - -@ExperimentalResourceApi -internal actual fun ByteArray.toImageBitmap(): ImageBitmap = toAndroidBitmap().asImageBitmap() - -private fun ByteArray.toAndroidBitmap(): Bitmap { - return BitmapFactory.decodeByteArray(this, 0, size); -} 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 new file mode 100644 index 0000000000..8856f503fc --- /dev/null +++ b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt @@ -0,0 +1,15 @@ +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 +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) } + return Font(path, LocalContext.current.assets, weight, style) +} \ No newline at end of file diff --git a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ImageResources.android.kt b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ImageResources.android.kt new file mode 100644 index 0000000000..b6caca1ea5 --- /dev/null +++ b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ImageResources.android.kt @@ -0,0 +1,8 @@ +package org.jetbrains.compose.resources + +import android.graphics.BitmapFactory +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap + +internal actual fun ByteArray.toImageBitmap(): ImageBitmap = + BitmapFactory.decodeByteArray(this, 0, size).asImageBitmap() \ No newline at end of file diff --git a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/Resource.android.kt b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/Resource.android.kt deleted file mode 100644 index 62a868e249..0000000000 --- a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/Resource.android.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package org.jetbrains.compose.resources - -import java.io.IOException - -@ExperimentalResourceApi -actual fun resource(path: String): Resource = AndroidResourceImpl(path) - -@ExperimentalResourceApi -private class AndroidResourceImpl(path: String) : AbstractResourceImpl(path) { - override suspend fun readBytes(): ByteArray { - val classLoader = Thread.currentThread().contextClassLoader ?: (::AndroidResourceImpl.javaClass.classLoader) - val resource = classLoader.getResourceAsStream(path) - if (resource != null) { - return resource.readBytes() - } else { - throw MissingResourceException(path) - } - } -} - -internal actual class MissingResourceException actual constructor(path: String) : - IOException("Missing resource with path: $path") diff --git a/components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/PlatformState.blocking.kt b/components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/PlatformState.blocking.kt new file mode 100644 index 0000000000..f54c6a3c93 --- /dev/null +++ b/components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/PlatformState.blocking.kt @@ -0,0 +1,14 @@ +package org.jetbrains.compose.resources + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import kotlinx.coroutines.runBlocking + +@Composable +internal actual fun rememberState(key: Any, getDefault: () -> T, block: suspend () -> T): State = remember(key) { + mutableStateOf( + runBlocking { block() } + ) +} \ No newline at end of file diff --git a/components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/Resource.blocking.kt b/components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/Resource.blocking.kt new file mode 100644 index 0000000000..e67caeed50 --- /dev/null +++ b/components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/Resource.blocking.kt @@ -0,0 +1,9 @@ +package org.jetbrains.compose.resources + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.newSingleThreadContext + +@OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class) +internal actual val cacheDispatcher: CoroutineDispatcher = newSingleThreadContext("resources_cache_ctx") \ No newline at end of file diff --git a/components/resources/library/src/blockingTest/kotlin/org/jetbrains/compose/resources/TestUtils.blocking.kt b/components/resources/library/src/blockingTest/kotlin/org/jetbrains/compose/resources/TestUtils.blocking.kt new file mode 100644 index 0000000000..1dc80a47eb --- /dev/null +++ b/components/resources/library/src/blockingTest/kotlin/org/jetbrains/compose/resources/TestUtils.blocking.kt @@ -0,0 +1,6 @@ +package org.jetbrains.compose.resources + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.runBlocking + +actual fun runBlockingTest(block: suspend CoroutineScope.() -> Unit) = runBlocking(block = block) \ No newline at end of file diff --git a/components/resources/library/src/commonButJSMain/kotlin/org/jetbrains/compose/resources/Resource.commonbutjs.kt b/components/resources/library/src/commonButJSMain/kotlin/org/jetbrains/compose/resources/Resource.commonbutjs.kt deleted file mode 100644 index ce0cba09cd..0000000000 --- a/components/resources/library/src/commonButJSMain/kotlin/org/jetbrains/compose/resources/Resource.commonbutjs.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package org.jetbrains.compose.resources - -import kotlinx.coroutines.runBlocking - -internal actual fun isSyncResourceLoadingSupported() = true - -@OptIn(ExperimentalResourceApi::class) -internal actual fun Resource.readBytesSync(): ByteArray = runBlocking { readBytes() } - diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ComposeResource.common.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ComposeResource.common.kt deleted file mode 100644 index 7206e0a56c..0000000000 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ComposeResource.common.kt +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package org.jetbrains.compose.resources - -import androidx.compose.runtime.* -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.painter.BitmapPainter -import androidx.compose.ui.graphics.painter.Painter -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.Density -import org.jetbrains.compose.resources.vector.xmldom.Element -import org.jetbrains.compose.resources.vector.parseVectorRoot -import androidx.compose.ui.unit.dp - -private val emptyImageBitmap: ImageBitmap by lazy { ImageBitmap(1, 1) } - -private val emptyImageVector: ImageVector by lazy { - ImageVector.Builder(defaultWidth = 1.dp, defaultHeight = 1.dp, viewportWidth = 1f, viewportHeight = 1f).build() -} - -/** - * Get and remember resource. While loading and if resource not exists result will be null. - */ -@ExperimentalResourceApi -@Composable -fun Resource.rememberImageBitmap(): LoadState { - val state: MutableState> = remember(this) { mutableStateOf(LoadState.Loading()) } - LaunchedEffect(this) { - state.value = try { - LoadState.Success(readBytes().toImageBitmap()) - } catch (e: Exception) { - LoadState.Error(e) - } - } - return state.value -} - -/** - * Get and remember resource. While loading and if resource not exists result will be null. - */ -@ExperimentalResourceApi -@Composable -fun Resource.rememberImageVector(density: Density): LoadState { - val state: MutableState> = remember(this, density) { mutableStateOf(LoadState.Loading()) } - LaunchedEffect(this, density) { - state.value = try { - LoadState.Success(readBytes().toImageVector(density)) - } catch (e: Exception) { - LoadState.Error(e) - } - } - return state.value -} - -private fun LoadState.orEmpty(emptyValue: T): T = when (this) { - is LoadState.Loading -> emptyValue - is LoadState.Success -> this.value - is LoadState.Error -> emptyValue -} - -/** - * Return current ImageBitmap or return empty while loading. - */ -@ExperimentalResourceApi -fun LoadState.orEmpty(): ImageBitmap = orEmpty(emptyImageBitmap) - -/** - * Return current ImageVector or return empty while loading. - */ -@ExperimentalResourceApi -fun LoadState.orEmpty(): ImageVector = orEmpty(emptyImageVector) - - -@OptIn(ExperimentalResourceApi::class) -@Composable -private fun Resource.rememberImageBitmapSync(): ImageBitmap = remember(this) { - readBytesSync().toImageBitmap() -} - -@OptIn(ExperimentalResourceApi::class) -@Composable -private fun Resource.rememberImageVectorSync(density: Density): ImageVector = remember(this, density) { - readBytesSync().toImageVector(density) -} - - -@OptIn(ExperimentalResourceApi::class) -@Composable -private fun painterResource( - res: String, - rememberImageBitmap: @Composable Resource.() -> ImageBitmap, - rememberImageVector: @Composable Resource.(Density) -> ImageVector -): Painter = - if (res.endsWith(".xml")) { - rememberVectorPainter(resource(res).rememberImageVector(LocalDensity.current)) - } else { - BitmapPainter(resource(res).rememberImageBitmap()) - } - -/** - * Return a Painter from the given resource path. - * Can load either a BitmapPainter for rasterized images (.png, .jpg) or - * a VectorPainter for XML Vector Drawables (.xml). - * - * XML Vector Drawables have the same format as for Android - * (https://developer.android.com/reference/android/graphics/drawable/VectorDrawable) - * except that external references to Android resources are not supported. - * - * Note that XML Vector Drawables are not supported for Web target currently. - */ -@ExperimentalResourceApi -@Composable -fun painterResource(res: String): Painter = - if (isSyncResourceLoadingSupported()) { - painterResource(res, {rememberImageBitmapSync()}, {density->rememberImageVectorSync(density)}) - } else { - painterResource(res, {rememberImageBitmap().orEmpty()}, {density->rememberImageVector(density).orEmpty()}) - } - - -internal expect fun isSyncResourceLoadingSupported(): Boolean - -@OptIn(ExperimentalResourceApi::class) -internal expect fun Resource.readBytesSync(): ByteArray - -internal expect fun ByteArray.toImageBitmap(): ImageBitmap - -internal expect fun parseXML(byteArray: ByteArray): Element - -internal fun ByteArray.toImageVector(density: Density): ImageVector = parseXML(this).parseVectorRoot(density) diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ExperimentalResourceApi.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ExperimentalResourceApi.kt deleted file mode 100644 index eb1e8a60be..0000000000 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ExperimentalResourceApi.kt +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package org.jetbrains.compose.resources - -@RequiresOptIn("This API is experimental and is likely to change in the future.") -annotation class ExperimentalResourceApi diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt new file mode 100644 index 0000000000..af61383de5 --- /dev/null +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt @@ -0,0 +1,25 @@ +package org.jetbrains.compose.resources + +import androidx.compose.runtime.Composable +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. + * + * @param id The resource ID of the font. + * @param weight The weight of the font. Default value is [FontWeight.Normal]. + * @param style The style of the font. Default value is [FontStyle.Normal]. + * + * @return The created [Font] object. + * + * @throws NotFoundException if the specified resource ID is not found. + */ +@ExperimentalResourceApi +@Composable +expect fun Font( + id: ResourceId, + 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 new file mode 100644 index 0000000000..c515d63620 --- /dev/null +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt @@ -0,0 +1,101 @@ +package org.jetbrains.compose.resources + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter +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 org.jetbrains.compose.resources.vector.toImageVector +import org.jetbrains.compose.resources.vector.xmldom.Element + + +/** + * Retrieves a [Painter] for the given [ResourceId]. + * Automatically select a type of the Painter depending on the file extension. + * + * @param id The ID of the resource to retrieve the [Painter] from. + * @return The [Painter] loaded from the resource. + */ +@ExperimentalResourceApi +@Composable +fun painterResource(id: ResourceId): Painter { + val filePath by rememberFilePath(id) + val isXml = filePath.endsWith(".xml", true) + if (isXml) { + return rememberVectorPainter(vectorResource(id)) + } else { + return BitmapPainter(imageResource(id)) + } +} + +private val emptyImageBitmap: ImageBitmap by lazy { ImageBitmap(1, 1) } + +/** + * Retrieves an ImageBitmap for the given resource ID. + * + * @param id The ID of the resource to load the ImageBitmap from. + * @return The ImageBitmap loaded from the resource. + */ +@ExperimentalResourceApi +@Composable +fun imageResource(id: ResourceId): ImageBitmap { + val resourceReader = LocalResourceReader.current + val imageBitmap by rememberState(id, { emptyImageBitmap }) { + val path = getPathById(id) + val cached = loadImage(path, resourceReader) { + ImageCache.Bitmap(it.toImageBitmap()) + } as ImageCache.Bitmap + cached.bitmap + } + return imageBitmap +} + +private val emptyImageVector: ImageVector by lazy { + ImageVector.Builder("emptyImageVector", 1.dp, 1.dp, 1f, 1f).build() +} + +/** + * Retrieves an ImageVector for the given resource ID. + * + * @param id The ID of the resource to load the ImageVector from. + * @return The ImageVector loaded from the resource. + */ +@ExperimentalResourceApi +@Composable +fun vectorResource(id: ResourceId): ImageVector { + val resourceReader = LocalResourceReader.current + val density = LocalDensity.current + val imageVector by rememberState(id, { emptyImageVector }) { + val path = getPathById(id) + val cached = loadImage(path, resourceReader) { + ImageCache.Vector(it.toXmlElement().toImageVector(density)) + } as ImageCache.Vector + cached.vector + } + return imageVector +} + +internal expect fun ByteArray.toImageBitmap(): ImageBitmap +internal expect fun ByteArray.toXmlElement(): Element + +private sealed interface ImageCache { + class Bitmap(val bitmap: ImageBitmap) : ImageCache + class Vector(val vector: ImageVector) : ImageCache +} + +private val imageCache = mutableMapOf() + +//@TestOnly +internal fun dropImageCache() { + imageCache.clear() +} + +private suspend fun loadImage( + path: String, + resourceReader: ResourceReader, + decode: (ByteArray) -> ImageCache +): ImageCache = imageCache.getOrPut(path) { decode(resourceReader.read(path)) } diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/LoadState.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/LoadState.kt deleted file mode 100644 index edeefd893c..0000000000 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/LoadState.kt +++ /dev/null @@ -1,114 +0,0 @@ -package org.jetbrains.compose.resources - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue - -/** - * Represents the load state of [T]. - */ -sealed class LoadState { - class Loading : LoadState() - data class Success(val value: T) : LoadState() - data class Error(val exception: Exception) : LoadState() -} - - -/** - * Load an item with type [T] asynchronously, and notify the caller about the load state. - * Whenever the load state changes (for example it succeeds or fails), the caller will be recomposed with the new state. - * The load will be cancelled when the [load] leaves the composition. - */ -@Composable -fun load(load: suspend () -> T): LoadState { - return load(Unit, load) -} - -/** - * Load an item with type [T] asynchronously. Returns null while loading or if the load has failed. - * Whenever the result changes, the caller will be recomposed with the new value. - * The load will be cancelled when the [loadOrNull] leaves the composition. - */ -@Composable -fun loadOrNull(load: suspend () -> T): T? { - return loadOrNull(Unit, load) -} - - -/** - * Load an item with type [T] asynchronously, and notify the caller about the load state. - * Whenever the load state changes (for example it succeeds or fails), the caller will be recomposed with the new state. - * The load will be cancelled and re-launched when [load] is recomposed with a different [key1]. - * The load will be cancelled when the [load] leaves the composition. - */ -@Composable -fun load(key1: Any?, load: suspend () -> T): LoadState { - return load(key1, Unit, load) -} - -/** - * Load an item with type [T] asynchronously. Returns null while loading or if the load has failed. - * Whenever the result changes, the caller will be recomposed with the new value. - * The load will be cancelled and re-launched when [loadOrNull] is recomposed with a different [key1]. - * The load will be cancelled when the [loadOrNull] leaves the composition. - */ -@Composable -fun loadOrNull(key1: Any?, load: suspend () -> T): T? { - return loadOrNull(key1, Unit, load) -} - -/** - * Load an item with type [T] asynchronously, and notify the caller about the load state. - * Whenever the load state changes (for example it succeeds or fails), the caller will be recomposed with the new state. - * The load will be cancelled and re-launched when [load] is recomposed with a different [key1] or [key2]. - * The load will be cancelled when the [load] leaves the composition. - */ -@Composable -fun load(key1: Any?, key2: Any?, load: suspend () -> T): LoadState { - return load(key1, key2, Unit, load) -} - -/** - * Load an item with type [T] asynchronously. Returns null while loading or if the load has failed. - * Whenever the result changes, the caller will be recomposed with the new value. - * The load will be cancelled and re-launched when [loadOrNull] is recomposed with a different [key1] or [key2]. - * The load will be cancelled when the [loadOrNull] leaves the composition. - */ -@Composable -fun loadOrNull(key1: Any?, key2: Any?, load: suspend () -> T): T? { - return loadOrNull(key1, key2, Unit, load) -} - -/** - * Load an item with type [T] asynchronously, and notify the caller about the load state. - * Whenever the load state changes (for example it succeeds or fails), the caller will be recomposed with the new state. - * The load will be cancelled and re-launched when [load] is recomposed with a different [key1], [key2] or [key3]. - * The load will be cancelled when the [load] leaves the composition. - */ -@Composable -fun load(key1: Any?, key2: Any?, key3: Any?, load: suspend () -> T): LoadState { - var state: LoadState by remember(key1, key2, key3) { mutableStateOf(LoadState.Loading()) } - LaunchedEffect(key1, key2, key3) { - state = try { - LoadState.Success(load()) - } catch (e: Exception) { - LoadState.Error(e) - } - } - return state -} - -/** - * Load an item with type [T] asynchronously. Returns null while loading or if the load has failed.. - * Whenever the result changes, the caller will be recomposed with the new value. - * The load will be cancelled and re-launched when [loadOrNull] is recomposed with a different [key1], [key2] or [key3]. - * The load will be cancelled when the [loadOrNull] leaves the composition. - */ -@Composable -fun loadOrNull(key1: Any?, key2: Any?, key3: Any?, load: suspend () -> T): T? { - val state = load(key1, key2, key3, load) - return (state as? LoadState.Success)?.value -} \ No newline at end of file diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PlatformState.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PlatformState.kt new file mode 100644 index 0000000000..eb84711edb --- /dev/null +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PlatformState.kt @@ -0,0 +1,16 @@ +package org.jetbrains.compose.resources + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State + +/** + * This is a platform-specific function that calculates and remembers a state. + * For all platforms except a JS it is a blocking function. + * On the JS platform it loads the state asynchronously and uses `getDefault` as an initial state value. + */ +@Composable +internal expect fun rememberState( + key: Any, + getDefault: () -> T, + block: suspend () -> T +): State \ No newline at end of file diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.common.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.common.kt deleted file mode 100644 index 59f4463e1d..0000000000 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.common.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package org.jetbrains.compose.resources - -/** - * Should implement equals() and hashCode() - */ -@ExperimentalResourceApi -interface Resource { - suspend fun readBytes(): ByteArray //todo in future use streaming -} - -/** - * Get a resource from /resources (for example, from commonMain/resources). - */ -@ExperimentalResourceApi -expect fun resource(path: String): Resource - -internal expect class MissingResourceException(path: String) - -@OptIn(ExperimentalResourceApi::class) -internal abstract class AbstractResourceImpl(val path: String) : Resource { - override fun equals(other: Any?): Boolean { - if (this === other) return true - return if (other is AbstractResourceImpl) { - path == other.path - } else { - false - } - } - - override fun hashCode(): Int { - return path.hashCode() - } -} 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 new file mode 100644 index 0000000000..ba5ac44486 --- /dev/null +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.kt @@ -0,0 +1,10 @@ +package org.jetbrains.compose.resources + +import kotlinx.coroutines.CoroutineDispatcher + +internal typealias ResourceId = String + +@RequiresOptIn("This API is experimental and is likely to change in the future.") +annotation class ExperimentalResourceApi + +internal expect val cacheDispatcher: CoroutineDispatcher \ 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 new file mode 100644 index 0000000000..c921974821 --- /dev/null +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceIndex.kt @@ -0,0 +1,14 @@ +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/ResourceReader.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt new file mode 100644 index 0000000000..f437519314 --- /dev/null +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt @@ -0,0 +1,27 @@ +package org.jetbrains.compose.resources + +import androidx.compose.runtime.staticCompositionLocalOf + +@ExperimentalResourceApi +class MissingResourceException(path: String) : Exception("Missing resource with path: $path") + +/** + * Reads the content of the resource file at the specified path and returns it as a byte array. + * + * @param path The path of the file to read in the resource's directory. + * @return The content of the file as a byte array. + */ +@ExperimentalResourceApi +expect suspend fun readResourceBytes(path: String): ByteArray + +internal interface ResourceReader { + suspend fun read(path: String): ByteArray +} + +internal val DefaultResourceReader: ResourceReader = object : ResourceReader { + @OptIn(ExperimentalResourceApi::class) + override suspend fun read(path: String): ByteArray = readResourceBytes(path) +} + +//ResourceReader provider will be overridden for tests +internal val LocalResourceReader = staticCompositionLocalOf { DefaultResourceReader } 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 new file mode 100644 index 0000000000..3b513fb693 --- /dev/null +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt @@ -0,0 +1,153 @@ +package org.jetbrains.compose.resources + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import kotlinx.coroutines.withContext +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]""") + +private sealed interface StringItem { + data class Value(val text: String) : StringItem + data class Array(val items: List) : StringItem +} + +private val parsedStringsCache = mutableMapOf>() + +//@TestOnly +internal fun dropStringsCache() { + parsedStringsCache.clear() +} + +private suspend fun getParsedStrings(path: String, resourceReader: ResourceReader): Map = + withContext(cacheDispatcher) { + 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) + } + strings + arrays + } + } + +/** + * Retrieves a string resource using the provided ID. + * + * @param id The ID of the string resource to retrieve. + * @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 { + val resourceReader = LocalResourceReader.current + val str by rememberState(id, { "" }) { loadString(id, resourceReader) } + return str +} + +/** + * Loads a string resource using the provided ID. + * + * @param id The ID of the string resource to load. + * @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) + +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!") + return item.text +} + +/** + * Retrieves a formatted string resource using the provided ID and arguments. + * + * @param id The ID of the string resource to retrieve. + * @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 +@Composable +fun getString(id: ResourceId, vararg formatArgs: Any): String { + val resourceReader = LocalResourceReader.current + val args = formatArgs.map { it.toString() } + val str by rememberState(id, { "" }) { loadString(id, args, resourceReader) } + return str +} + +/** + * Loads a formatted string resource using the provided ID and arguments. + * + * @param id The ID of the string resource to load. + * @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, + formatArgs.map { it.toString() }, + DefaultResourceReader +) + +private suspend fun loadString(id: ResourceId, args: List, resourceReader: ResourceReader): String { + val str = loadString(id, resourceReader) + return SimpleStringFormatRegex.replace(str) { matchResult -> + args[matchResult.groupValues[1].toInt() - 1] + } +} + +/** + * Retrieves a list of strings from a string array resource. + * + * @param id The ID of the string array resource. + * @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 { + val resourceReader = LocalResourceReader.current + val array by rememberState(id, { emptyList() }) { loadStringArray(id, resourceReader) } + return array +} + +/** + * Loads a string array from a resource file. + * + * @param id The ID of the string array resource. + * @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) + +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!") + return item.items +} + +private fun NodeList.getElementsWithName(name: String): List = + List(length) { item(it) } + .filterIsInstance() + .filter { it.localName == name } \ No newline at end of file diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/XmlVectorParser.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/XmlVectorParser.kt index c669f6e9c8..ff17801580 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/XmlVectorParser.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/XmlVectorParser.kt @@ -17,8 +17,8 @@ package org.jetbrains.compose.resources.vector import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap @@ -34,9 +34,10 @@ import androidx.compose.ui.graphics.vector.DefaultTranslationY import androidx.compose.ui.graphics.vector.EmptyPath import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.addPathNodes -import org.jetbrains.compose.resources.vector.BuildContext.Group import androidx.compose.ui.unit.Density -import org.jetbrains.compose.resources.vector.xmldom.* +import org.jetbrains.compose.resources.vector.BuildContext.Group +import org.jetbrains.compose.resources.vector.xmldom.Element +import org.jetbrains.compose.resources.vector.xmldom.Node // Parsing logic is the same as in Android implementation @@ -68,7 +69,7 @@ private class BuildContext { } } -internal fun Element.parseVectorRoot(density: Density): ImageVector { +internal fun Element.toImageVector(density: Density): ImageVector { val context = BuildContext() val builder = ImageVector.Builder( defaultWidth = attributeOrNull(ANDROID_NS, "width").parseDp(density), @@ -167,7 +168,6 @@ private fun Element.parseGradient(): Brush? { } } -@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS") private fun Element.parseLinearGradient() = Brush.linearGradient( colorStops = parseColorStops(), start = Offset( @@ -181,7 +181,6 @@ private fun Element.parseLinearGradient() = Brush.linearGradient( tileMode = attributeOrNull(ANDROID_NS, "tileMode")?.let(::parseTileMode) ?: TileMode.Clamp ) -@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS") private fun Element.parseRadialGradient() = Brush.radialGradient( colorStops = parseColorStops(), center = Offset( @@ -192,7 +191,6 @@ private fun Element.parseRadialGradient() = Brush.radialGradient( tileMode = attributeOrNull(ANDROID_NS, "tileMode")?.let(::parseTileMode) ?: TileMode.Clamp ) -@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS") private fun Element.parseSweepGradient() = Brush.sweepGradient( colorStops = parseColorStops(), center = Offset( diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/Node.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/Node.kt index f8f97c113c..8f663be5f5 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/Node.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/Node.kt @@ -4,6 +4,8 @@ package org.jetbrains.compose.resources.vector.xmldom * XML DOM Node. */ internal interface Node { + val textContent: String? + val nodeName: String val localName: String diff --git a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/RecompositionsCounter.kt b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/RecompositionsCounter.kt new file mode 100644 index 0000000000..8c2501b6e4 --- /dev/null +++ b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/RecompositionsCounter.kt @@ -0,0 +1,14 @@ +package org.jetbrains.compose.resources + +import androidx.compose.runtime.Composable + +internal class RecompositionsCounter { + var count = 0 + private set + + @Composable + fun content(block: @Composable () -> Unit) { + count++ + block() + } +} \ No newline at end of file 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 6d47e32c03..56c8d5f3af 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 @@ -7,17 +7,60 @@ package org.jetbrains.compose.resources import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNotEquals @OptIn(ExperimentalResourceApi::class) class ResourceTest { @Test - fun testResourceEquals() { - assertEquals(resource("a"), resource("a")) + fun testResourceEquals() = runBlockingTest { + assertEquals(getPathById("a"), getPathById("a")) } @Test - fun testResourceNotEquals() { - assertNotEquals(resource("a"), resource("b")) + fun testResourceNotEquals() = runBlockingTest { + assertNotEquals(getPathById("a"), getPathById("b")) + } + + @Test + fun testMissingResource() = runBlockingTest { + assertFailsWith { + readResourceBytes("missing.png") + } + val error = assertFailsWith { + loadString("unknown_id") + } + assertEquals("String ID=`unknown_id` is not found!", error.message) + } + + @Test + fun testReadFileResource() = runBlockingTest { + val bytes = readResourceBytes("strings.xml") + assertEquals( + """ + + Compose Resources App + 😊 Hello world! + Hello, %1${'$'}s! You have %2${'$'}d new messages. + + item 1 + item 2 + item 3 + + + + """.trimIndent(), + bytes.decodeToString() + ) + } + + @Test + fun testLoadStringResource() = runBlockingTest { + assertEquals("Compose Resources App", loadString("app_name")) + assertEquals( + "Hello, test-name! You have 42 new messages.", + loadString("str_template", "test-name", 42) + ) + assertEquals(listOf("item 1", "item 2", "item 3"), loadStringArray("str_arr")) } } diff --git a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestResourceReader.kt b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestResourceReader.kt new file mode 100644 index 0000000000..3255ccf857 --- /dev/null +++ b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestResourceReader.kt @@ -0,0 +1,11 @@ +package org.jetbrains.compose.resources + +internal class TestResourceReader : ResourceReader { + private val readPathsList = mutableListOf() + val readPaths: List get() = readPathsList + + override suspend fun read(path: String): ByteArray { + readPathsList.add(path) + return DefaultResourceReader.read(path) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..a8b4ef6c77 --- /dev/null +++ b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt @@ -0,0 +1,5 @@ +package org.jetbrains.compose.resources + +import kotlinx.coroutines.CoroutineScope + +expect fun runBlockingTest(block: suspend CoroutineScope.() -> Unit) \ No newline at end of file diff --git a/components/resources/library/src/androidInstrumentedTest/resources/1.png b/components/resources/library/src/commonTest/resources/1.png similarity index 100% rename from components/resources/library/src/androidInstrumentedTest/resources/1.png rename to components/resources/library/src/commonTest/resources/1.png diff --git a/components/resources/library/src/androidInstrumentedTest/resources/2.png b/components/resources/library/src/commonTest/resources/2.png similarity index 100% rename from components/resources/library/src/androidInstrumentedTest/resources/2.png rename to components/resources/library/src/commonTest/resources/2.png diff --git a/components/resources/library/src/commonTest/resources/font_awesome.otf b/components/resources/library/src/commonTest/resources/font_awesome.otf new file mode 100644 index 0000000000..401ec0f36e Binary files /dev/null and b/components/resources/library/src/commonTest/resources/font_awesome.otf differ diff --git a/components/resources/library/src/commonTest/resources/strings.xml b/components/resources/library/src/commonTest/resources/strings.xml new file mode 100644 index 0000000000..fd2f9a9bca --- /dev/null +++ b/components/resources/library/src/commonTest/resources/strings.xml @@ -0,0 +1,10 @@ + + Compose Resources App + 😊 Hello world! + Hello, %1$s! You have %2$d new messages. + + item 1 + item 2 + item 3 + + diff --git a/components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/Resource.desktop.kt b/components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/Resource.desktop.kt deleted file mode 100644 index 8aaf0ab74c..0000000000 --- a/components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/Resource.desktop.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package org.jetbrains.compose.resources - -import java.io.IOException - -@ExperimentalResourceApi -actual fun resource(path: String): Resource = DesktopResourceImpl(path) - -@ExperimentalResourceApi -private class DesktopResourceImpl(path: String) : AbstractResourceImpl(path) { - override suspend fun readBytes(): ByteArray { - val classLoader = Thread.currentThread().contextClassLoader ?: (::DesktopResourceImpl.javaClass.classLoader) - val resource = classLoader.getResourceAsStream(path) - if (resource != null) { - return resource.readBytes() - } else { - throw MissingResourceException(path) - } - } -} - -internal actual class MissingResourceException actual constructor(path: String) : - IOException("Missing resource with path: $path") 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 new file mode 100644 index 0000000000..5a88b45ccc --- /dev/null +++ b/components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.desktop.kt @@ -0,0 +1,107 @@ +package org.jetbrains.compose.resources + +import androidx.compose.foundation.Image +import androidx.compose.material3.Text +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.runComposeUiTest +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalResourceApi::class, ExperimentalTestApi::class) +class ComposeResourceTest { + + @Before + fun dropCaches() { + dropStringsCache() + dropImageCache() + } + + @Test + fun testCountRecompositions() = runComposeUiTest { + runBlockingTest { + val imagePathFlow = MutableStateFlow("1.png") + val recompositionsCounter = RecompositionsCounter() + setContent { + val path by imagePathFlow.collectAsState() + val res = imageResource(path) + recompositionsCounter.content { + Image(bitmap = res, contentDescription = null) + } + } + awaitIdle() + imagePathFlow.emit("2.png") + awaitIdle() + assertEquals(2, recompositionsCounter.count) + } + } + + @Test + fun testImageResourceCache() = runComposeUiTest { + runBlockingTest { + val testResourceReader = TestResourceReader() + val imagePathFlow = MutableStateFlow("1.png") + setContent { + CompositionLocalProvider(LocalResourceReader provides testResourceReader) { + val path by imagePathFlow.collectAsState() + Image(painterResource(path), null) + } + } + awaitIdle() + imagePathFlow.emit("2.png") + awaitIdle() + imagePathFlow.emit("1.png") + awaitIdle() + + assertEquals( + expected = listOf("1.png", "2.png"), //no second read of 1.png + actual = testResourceReader.readPaths + ) + } + } + + @Test + fun testStringResourceCache() = runComposeUiTest { + runBlockingTest { + val testResourceReader = TestResourceReader() + val stringIdFlow = MutableStateFlow("app_name") + setContent { + CompositionLocalProvider(LocalResourceReader provides testResourceReader) { + val textId by stringIdFlow.collectAsState() + Text(getString(textId)) + Text(getStringArray("str_arr").joinToString()) + } + } + awaitIdle() + stringIdFlow.emit("hello") + awaitIdle() + stringIdFlow.emit("app_name") + awaitIdle() + + assertEquals( + expected = listOf("strings.xml"), //just one string.xml read + actual = testResourceReader.readPaths + ) + } + } + + @Test + fun testReadStringResource() = runComposeUiTest { + runBlockingTest { + setContent { + assertEquals("Compose Resources App", getString("app_name")) + assertEquals( + "Hello, test-name! You have 42 new messages.", + getString("str_template", "test-name", 42) + ) + assertEquals(listOf("item 1", "item 2", "item 3"), getStringArray("str_arr")) + } + awaitIdle() + } + } +} diff --git a/components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt b/components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt deleted file mode 100644 index 2803875019..0000000000 --- a/components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt +++ /dev/null @@ -1,61 +0,0 @@ -package org.jetbrains.compose.resources - -import androidx.compose.foundation.Image -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.test.junit4.createComposeRule -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertEquals -import org.junit.Rule -import org.junit.Test - -@OptIn(ExperimentalResourceApi::class, ExperimentalCoroutinesApi::class) -class ComposeResourceTest { - - @get:Rule - val rule = createComposeRule() - - @Test - fun testMissingResource() = runTest (UnconfinedTestDispatcher()) { - var recompositionCount = 0 - rule.setContent { - CountRecompositions(resource("missing.png").rememberImageBitmap().orEmpty()) { - recompositionCount++ - } - } - rule.awaitIdle() - assertEquals(2, recompositionCount) - } - - @Test - fun testCountRecompositions() = runTest (UnconfinedTestDispatcher()) { - val mutableStateFlow = MutableStateFlow(true) - var recompositionCount = 0 - rule.setContent { - val state: Boolean by mutableStateFlow.collectAsState(true) - val resource = resource(if (state) "1.png" else "2.png") - CountRecompositions(resource.rememberImageBitmap().orEmpty()) { - recompositionCount++ - } - } - rule.awaitIdle() - mutableStateFlow.value = false - rule.awaitIdle() - assertEquals(4, recompositionCount) - } - -} - -@Composable -private fun CountRecompositions(imageBitmap: ImageBitmap?, onRecomposition: () -> Unit) { - onRecomposition() - if (imageBitmap != null) { - Image(bitmap = imageBitmap, contentDescription = null) - } -} diff --git a/components/resources/library/src/desktopTest/resources/1.png b/components/resources/library/src/desktopTest/resources/1.png deleted file mode 100644 index c11cb2ec65..0000000000 Binary files a/components/resources/library/src/desktopTest/resources/1.png and /dev/null differ diff --git a/components/resources/library/src/desktopTest/resources/2.png b/components/resources/library/src/desktopTest/resources/2.png deleted file mode 100644 index 5fa326654e..0000000000 Binary files a/components/resources/library/src/desktopTest/resources/2.png and /dev/null differ diff --git a/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/Resource.ios.kt b/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/Resource.ios.kt deleted file mode 100644 index 9d9be82c9a..0000000000 --- a/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/Resource.ios.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package org.jetbrains.compose.resources - -import kotlinx.cinterop.addressOf -import kotlinx.cinterop.usePinned -import platform.Foundation.NSBundle -import platform.Foundation.NSData -import platform.Foundation.NSFileManager -import platform.posix.memcpy - -@ExperimentalResourceApi -actual fun resource(path: String): Resource = UIKitResourceImpl(path) - -@ExperimentalResourceApi -private class UIKitResourceImpl(path: String) : AbstractResourceImpl(path) { - override suspend fun readBytes(): ByteArray { - val fileManager = NSFileManager.defaultManager() - // todo: support fallback path at bundle root? - val composeResourcesPath = NSBundle.mainBundle.resourcePath + "/compose-resources/" + path - val contentsAtPath: NSData? = fileManager.contentsAtPath(composeResourcesPath) - if (contentsAtPath != null) { - val byteArray = ByteArray(contentsAtPath.length.toInt()) - byteArray.usePinned { - memcpy(it.addressOf(0), contentsAtPath.bytes, contentsAtPath.length) - } - return byteArray - } else { - throw MissingResourceException(path) - } - } -} - -internal actual class MissingResourceException actual constructor(path: String) : - Exception("Missing resource with path: $path") diff --git a/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.ios.kt b/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.ios.kt new file mode 100644 index 0000000000..6865a3bad9 --- /dev/null +++ b/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.ios.kt @@ -0,0 +1,20 @@ +package org.jetbrains.compose.resources + +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.usePinned +import platform.Foundation.NSBundle +import platform.Foundation.NSFileManager +import platform.posix.memcpy + +@ExperimentalResourceApi +actual suspend fun readResourceBytes(path: String): ByteArray { + val fileManager = NSFileManager.defaultManager() + // todo: support fallback path at bundle root? + val composeResourcesPath = NSBundle.mainBundle.resourcePath + "/compose-resources/" + path + val contentsAtPath = fileManager.contentsAtPath(composeResourcesPath) ?: throw MissingResourceException(path) + return ByteArray(contentsAtPath.length.toInt()).apply { + usePinned { + memcpy(it.addressOf(0), contentsAtPath.bytes, contentsAtPath.length) + } + } +} \ No newline at end of file diff --git a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ImageResources.js.kt b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ImageResources.js.kt new file mode 100644 index 0000000000..f9c23a0973 --- /dev/null +++ b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ImageResources.js.kt @@ -0,0 +1,13 @@ +package org.jetbrains.compose.resources + +import org.jetbrains.compose.resources.vector.xmldom.Element +import org.jetbrains.compose.resources.vector.xmldom.ElementImpl +import org.jetbrains.compose.resources.vector.xmldom.MalformedXMLException +import org.w3c.dom.parsing.DOMParser + +internal actual fun ByteArray.toXmlElement(): Element { + val xmlString = decodeToString() + val xmlDom = DOMParser().parseFromString(xmlString, "application/xml") + val domElement = xmlDom.documentElement ?: throw MalformedXMLException("missing documentElement") + return ElementImpl(domElement) +} \ No newline at end of file diff --git a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/PlatformState.js.kt b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/PlatformState.js.kt new file mode 100644 index 0000000000..9ca7dd3ed9 --- /dev/null +++ b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/PlatformState.js.kt @@ -0,0 +1,16 @@ +package org.jetbrains.compose.resources + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember + +@Composable +internal actual fun rememberState(key: Any, getDefault: () -> T, block: suspend () -> T): State { + val state = remember(key) { mutableStateOf(getDefault()) } + LaunchedEffect(key) { + state.value = block() + } + return state +} \ No newline at end of file diff --git a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt index e982712ccb..c560bfc90b 100644 --- a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt +++ b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt @@ -1,18 +1,9 @@ -/* - * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - package org.jetbrains.compose.resources -import kotlinx.browser.window -import kotlinx.coroutines.await -import org.jetbrains.compose.resources.vector.xmldom.Element -import org.jetbrains.compose.resources.vector.xmldom.ElementImpl -import org.jetbrains.compose.resources.vector.xmldom.MalformedXMLException -import org.khronos.webgl.ArrayBuffer -import org.khronos.webgl.Int8Array -import org.w3c.dom.parsing.DOMParser +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +internal actual val cacheDispatcher: CoroutineDispatcher = Dispatchers.Default /** * Represents the configuration object for web resources. @@ -22,31 +13,24 @@ import org.w3c.dom.parsing.DOMParser @Suppress("unused") @ExperimentalResourceApi object WebResourcesConfiguration { + internal var getResourcePath: (path: String) -> String = { "./$it" } /** - * An internal default factory method for creating [Resource] from a given path. - * It can be changed at runtime by using [setResourceFactory]. - */ - @ExperimentalResourceApi - internal var jsResourceImplFactory: (path: String) -> Resource = { urlResource("./$it") } - - /** - * Sets a custom factory for the [resource] function to create [Resource] instances. - * Once set, the [factory] will effectively define the implementation of the [resource] function. + * Sets a customization function for resource path. This allows you to modify the resource path + * before it is used. * - * @param factory A lambda that accepts a path and produces a [Resource] instance. - * @see configureWebResources for examples on how to use this function. + * @param map the mapping function that takes a path String and returns a modified path String */ @ExperimentalResourceApi - fun setResourceFactory(factory: (path: String) -> Resource) { - jsResourceImplFactory = factory + fun resourcePathMapping(map: (path: String) -> String) { + getResourcePath = map } } /** * Configures the web resources behavior. * - * Allows users to override default behavior and provide custom logic for generating [Resource] instances. + * Allows users to override default behavior and provide custom logic for generating resource's paths. * * @param configure Configuration lambda applied to [WebResourcesConfiguration]. * @see WebResourcesConfiguration For detailed configuration options. @@ -54,10 +38,10 @@ object WebResourcesConfiguration { * Examples: * ``` * configureWebResources { - * setResourceFactory { path -> urlResource("/myApp1/resources/$path") } + * resourcePathMapping { path -> "/myApp1/resources/$path" } * } * configureWebResources { - * setResourceFactory { path -> urlResource("https://mycdn.com/myApp1/res/$path") } + * resourcePathMapping { path -> "https://mycdn.com/myApp1/res/$path" } * } * ``` */ @@ -65,53 +49,4 @@ object WebResourcesConfiguration { @ExperimentalResourceApi fun configureWebResources(configure: WebResourcesConfiguration.() -> Unit) { WebResourcesConfiguration.configure() -} - -/** - * Generates a [Resource] instance based on the provided [path]. - * - * By default, the path is treated as relative to the current URL segment. - * The default behaviour can be overridden by using [configureWebResources]. - * - * @param path The path or resource id used to generate the [Resource] instance. - * @return A [Resource] instance corresponding to the provided path. - */ -@ExperimentalResourceApi -actual fun resource(path: String): Resource = WebResourcesConfiguration.jsResourceImplFactory(path) - -/** - * Creates a [Resource] instance based on the provided [url]. - * - * @param url The URL used to access the [Resource]. - * @return A [Resource] instance accessible by the given URL. - */ -@ExperimentalResourceApi -fun urlResource(url: String): Resource = JSUrlResourceImpl(url) - -@ExperimentalResourceApi -private class JSUrlResourceImpl(url: String) : AbstractResourceImpl(url) { - override suspend fun readBytes(): ByteArray { - val response = window.fetch(path).await() - if (!response.ok) { - throw MissingResourceException(path) - } - return response.arrayBuffer().await().toByteArray() - } -} - -private fun ArrayBuffer.toByteArray() = Int8Array(this, 0, byteLength).unsafeCast() - -internal actual class MissingResourceException actual constructor(path: String) : - Exception("Missing resource with path: $path") - -internal actual fun parseXML(byteArray: ByteArray): Element { - val xmlString = byteArray.decodeToString() - val xmlDom = DOMParser().parseFromString(xmlString, "application/xml") - val domElement = xmlDom.documentElement ?: throw MalformedXMLException("missing documentElement") - return ElementImpl(domElement) -} - -internal actual fun isSyncResourceLoadingSupported() = false - -@OptIn(ExperimentalResourceApi::class) -internal actual fun Resource.readBytesSync(): ByteArray = throw UnsupportedOperationException() \ No newline at end of file +} \ No newline at end of file diff --git a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.js.kt b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.js.kt new file mode 100644 index 0000000000..4897a4eb10 --- /dev/null +++ b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.js.kt @@ -0,0 +1,19 @@ +package org.jetbrains.compose.resources + +import kotlinx.browser.window +import kotlinx.coroutines.await +import org.khronos.webgl.ArrayBuffer +import org.khronos.webgl.Int8Array + +private fun ArrayBuffer.toByteArray(): ByteArray = + Int8Array(this, 0, byteLength).unsafeCast() + +@ExperimentalResourceApi +actual suspend fun readResourceBytes(path: String): ByteArray { + val resPath = WebResourcesConfiguration.getResourcePath(path) + val response = window.fetch(resPath).await() + if (!response.ok) { + throw MissingResourceException(resPath) + } + return response.arrayBuffer().await().toByteArray() +} \ No newline at end of file diff --git a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/xmldom/ElementImpl.kt b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/ElementImpl.kt similarity index 88% rename from components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/xmldom/ElementImpl.kt rename to components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/ElementImpl.kt index 4e475b0cf8..8a9b566a18 100644 --- a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/xmldom/ElementImpl.kt +++ b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/ElementImpl.kt @@ -3,6 +3,8 @@ package org.jetbrains.compose.resources.vector.xmldom import org.w3c.dom.Element as DomElement internal class ElementImpl(val element: DomElement): NodeImpl(element), Element { + override val textContent: String? + get() = element.textContent override val localName: String get() = element.localName diff --git a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/xmldom/NodeImpl.kt b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/NodeImpl.kt similarity index 93% rename from components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/xmldom/NodeImpl.kt rename to components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/NodeImpl.kt index edff59e15a..09a0f44200 100644 --- a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/xmldom/NodeImpl.kt +++ b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/NodeImpl.kt @@ -1,9 +1,12 @@ package org.jetbrains.compose.resources.vector.xmldom -import org.w3c.dom.Node as DomNode import org.w3c.dom.Element as DomElement +import org.w3c.dom.Node as DomNode internal open class NodeImpl(val n: DomNode): Node { + override val textContent: String? + get() = n.textContent + override val nodeName: String get() = n.nodeName diff --git a/components/resources/library/src/jsTest/kotlin/org/jetbrains/compose/resources/TestUtils.js.kt b/components/resources/library/src/jsTest/kotlin/org/jetbrains/compose/resources/TestUtils.js.kt new file mode 100644 index 0000000000..1884a096d8 --- /dev/null +++ b/components/resources/library/src/jsTest/kotlin/org/jetbrains/compose/resources/TestUtils.js.kt @@ -0,0 +1,9 @@ +package org.jetbrains.compose.resources + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.promise + +@OptIn(DelicateCoroutinesApi::class) +actual fun runBlockingTest(block: suspend CoroutineScope.() -> Unit): dynamic = GlobalScope.promise(block = block) \ No newline at end of file diff --git a/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/ImageResources.jvmAndAndroid.kt b/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/ImageResources.jvmAndAndroid.kt new file mode 100644 index 0000000000..87c8a4446e --- /dev/null +++ b/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/ImageResources.jvmAndAndroid.kt @@ -0,0 +1,15 @@ +package org.jetbrains.compose.resources + +import org.jetbrains.compose.resources.vector.xmldom.Element +import org.jetbrains.compose.resources.vector.xmldom.ElementImpl +import org.xml.sax.InputSource +import java.io.ByteArrayInputStream +import javax.xml.parsers.DocumentBuilderFactory + +internal actual fun ByteArray.toXmlElement(): Element = ElementImpl( + DocumentBuilderFactory.newInstance() + .apply { isNamespaceAware = true } + .newDocumentBuilder() + .parse(InputSource(ByteArrayInputStream(this))) + .documentElement +) \ No newline at end of file diff --git a/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/Resource.jvmandandroid.kt b/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/Resource.jvmandandroid.kt deleted file mode 100644 index f4866cc5a8..0000000000 --- a/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/Resource.jvmandandroid.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package org.jetbrains.compose.resources - -import org.jetbrains.compose.resources.vector.xmldom.Element -import org.jetbrains.compose.resources.vector.xmldom.ElementImpl -import org.xml.sax.InputSource -import java.io.ByteArrayInputStream -import javax.xml.parsers.DocumentBuilderFactory - -internal actual fun parseXML(byteArray: ByteArray): Element = - ElementImpl( - DocumentBuilderFactory - .newInstance().apply { - isNamespaceAware = true - } - .newDocumentBuilder() - .parse(InputSource(ByteArrayInputStream(byteArray))) - .documentElement) diff --git a/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.jvmAndAndroid.kt b/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.jvmAndAndroid.kt new file mode 100644 index 0000000000..c817770744 --- /dev/null +++ b/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.jvmAndAndroid.kt @@ -0,0 +1,10 @@ +package org.jetbrains.compose.resources + +private object JvmResourceReader + +@ExperimentalResourceApi +actual suspend fun readResourceBytes(path: String): ByteArray { + val classLoader = Thread.currentThread().contextClassLoader ?: JvmResourceReader.javaClass.classLoader + val resource = classLoader.getResourceAsStream(path) ?: throw MissingResourceException(path) + return resource.readBytes() +} \ No newline at end of file diff --git a/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/xmldom/ElementImpl.kt b/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/ElementImpl.kt similarity index 100% rename from components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/xmldom/ElementImpl.kt rename to components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/ElementImpl.kt diff --git a/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/xmldom/NodeImpl.kt b/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/NodeImpl.kt similarity index 92% rename from components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/xmldom/NodeImpl.kt rename to components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/NodeImpl.kt index 5392327ba0..cb74da5984 100644 --- a/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/xmldom/NodeImpl.kt +++ b/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/NodeImpl.kt @@ -1,9 +1,12 @@ package org.jetbrains.compose.resources.vector.xmldom -import org.w3c.dom.Node as DomNode import org.w3c.dom.Element as DomElement +import org.w3c.dom.Node as DomNode internal open class NodeImpl(val n: DomNode): Node { + override val textContent: String? + get() = n.textContent + override val nodeName: String get() = n.nodeName override val localName: String diff --git a/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/Resource.macos.kt b/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/Resource.macos.kt deleted file mode 100644 index becf5343ef..0000000000 --- a/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/Resource.macos.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package org.jetbrains.compose.resources - -import kotlinx.cinterop.addressOf -import kotlinx.cinterop.usePinned -import platform.Foundation.NSData -import platform.Foundation.NSFileManager -import platform.posix.memcpy - -@ExperimentalResourceApi -actual fun resource(path: String): Resource = MacOSResourceImpl(path) - -@ExperimentalResourceApi -private class MacOSResourceImpl(path: String) : AbstractResourceImpl(path) { - override suspend fun readBytes(): ByteArray { - val currentDirectoryPath = NSFileManager.defaultManager().currentDirectoryPath - val contentsAtPath: NSData? = NSFileManager.defaultManager().run { - //todo in future bundle resources with app and use all sourceSets (skikoMain, nativeMain) - contentsAtPath("$currentDirectoryPath/src/macosMain/resources/$path") - ?: contentsAtPath("$currentDirectoryPath/src/commonMain/resources/$path") - } - if (contentsAtPath != null) { - val byteArray = ByteArray(contentsAtPath.length.toInt()) - byteArray.usePinned { - memcpy(it.addressOf(0), contentsAtPath.bytes, contentsAtPath.length) - } - return byteArray - } else { - throw MissingResourceException(path) - } - } -} - -internal actual class MissingResourceException actual constructor(path: String) : - Exception("Missing resource with path: $path") - diff --git a/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.macos.kt b/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.macos.kt new file mode 100644 index 0000000000..d87d05a161 --- /dev/null +++ b/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.macos.kt @@ -0,0 +1,21 @@ +package org.jetbrains.compose.resources + +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.usePinned +import platform.Foundation.NSFileManager +import platform.posix.memcpy + +@ExperimentalResourceApi +actual suspend fun readResourceBytes(path: String): ByteArray { + val currentDirectoryPath = NSFileManager.defaultManager().currentDirectoryPath + val contentsAtPath = NSFileManager.defaultManager().run { + //todo in future bundle resources with app and use all sourceSets (skikoMain, nativeMain) + contentsAtPath("$currentDirectoryPath/src/macosMain/resources/$path") + ?: contentsAtPath("$currentDirectoryPath/src/commonMain/resources/$path") + } ?: throw MissingResourceException(path) + return ByteArray(contentsAtPath.length.toInt()).apply { + usePinned { + memcpy(it.addressOf(0), contentsAtPath.bytes, contentsAtPath.length) + } + } +} \ No newline at end of file diff --git a/components/resources/library/src/nativeMain/kotlin/org/jetbrains/compose/resources/ImageResources.native.kt b/components/resources/library/src/nativeMain/kotlin/org/jetbrains/compose/resources/ImageResources.native.kt new file mode 100644 index 0000000000..351b1257b9 --- /dev/null +++ b/components/resources/library/src/nativeMain/kotlin/org/jetbrains/compose/resources/ImageResources.native.kt @@ -0,0 +1,6 @@ +package org.jetbrains.compose.resources + +import org.jetbrains.compose.resources.vector.xmldom.Element +import org.jetbrains.compose.resources.vector.xmldom.parse + +internal actual fun ByteArray.toXmlElement(): Element = parse(decodeToString()) diff --git a/components/resources/library/src/nativeMain/kotlin/org/jetbrains/compose/resources/Resource.native.kt b/components/resources/library/src/nativeMain/kotlin/org/jetbrains/compose/resources/Resource.native.kt deleted file mode 100644 index ab823d68f8..0000000000 --- a/components/resources/library/src/nativeMain/kotlin/org/jetbrains/compose/resources/Resource.native.kt +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package org.jetbrains.compose.resources - -import org.jetbrains.compose.resources.vector.xmldom.Element -import org.jetbrains.compose.resources.vector.xmldom.parse - -internal actual fun parseXML(byteArray: ByteArray): Element = parse(byteArray.decodeToString()) diff --git a/components/resources/library/src/nativeMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt b/components/resources/library/src/nativeMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt index 7e848ec27b..d31065ae54 100644 --- a/components/resources/library/src/nativeMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt +++ b/components/resources/library/src/nativeMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt @@ -4,12 +4,17 @@ */ package org.jetbrains.compose.resources.vector.xmldom -import org.jetbrains.compose.resources.vector.xmldom.MalformedXMLException -import platform.Foundation.* +import platform.Foundation.NSError +import platform.Foundation.NSString +import platform.Foundation.NSUTF8StringEncoding +import platform.Foundation.NSXMLParser +import platform.Foundation.NSXMLParserDelegateProtocol +import platform.Foundation.dataUsingEncoding import platform.darwin.NSObject internal fun parse(xml: String): Element { val parser = DomXmlParser() + @Suppress("CAST_NEVER_SUCCEEDS") NSXMLParser((xml as NSString).dataUsingEncoding(NSUTF8StringEncoding)!!).apply { shouldReportNamespacePrefixes = true shouldProcessNamespaces = true @@ -18,11 +23,15 @@ internal fun parse(xml: String): Element { return parser.root!! } -private class ElementImpl(override val localName: String, - override val nodeName: String, - override val namespaceURI: String, - val prefixMap: Map, - val attributes: Map): Element { +private class ElementImpl( + override val localName: String, + override val nodeName: String, + override val namespaceURI: String, + val prefixMap: Map, + val attributes: Map +) : Element { + + override var textContent: String? = null override val childNodes: NodeList get() = object : NodeList { @@ -43,15 +52,13 @@ private class ElementImpl(override val localName: String, return getAttribute(attrKey) } - override fun getAttribute(name: String): String = attributes[name] as String? ?:"" + override fun getAttribute(name: String): String = attributes[name] as String? ?: "" - override fun lookupPrefix(uri: String): String = prefixMap[uri]?:"" + override fun lookupPrefix(namespaceURI: String): String = prefixMap[namespaceURI] ?: "" } -@Suppress("CONFLICTING_OVERLOADS") -private class DomXmlParser( - -) : NSObject(), NSXMLParserDelegateProtocol { +@Suppress("CONFLICTING_OVERLOADS", "PARAMETER_NAME_CHANGED_ON_OVERRIDE") +private class DomXmlParser : NSObject(), NSXMLParserDelegateProtocol { val curPrefixMapInverted = mutableMapOf() @@ -68,8 +75,13 @@ private class DomXmlParser( qualifiedName: String?, attributes: Map ) { - val node = ElementImpl(didStartElement, qualifiedName!!, namespaceURI?:"", - curPrefixMap, attributes) + val node = ElementImpl( + didStartElement, + qualifiedName!!, + namespaceURI ?: "", + curPrefixMap, + attributes + ) if (root == null) root = node @@ -79,6 +91,10 @@ private class DomXmlParser( nodeStack.add(node) } + override fun parser(parser: NSXMLParser, foundCharacters: String) { + nodeStack.lastOrNull()?.textContent = foundCharacters + } + override fun parser( parser: NSXMLParser, didEndElement: String, @@ -89,9 +105,10 @@ private class DomXmlParser( assert(node.localName.equals(didEndElement)) } - override fun parser(parser: NSXMLParser, - didStartMappingPrefix: String, - toURI: String + override fun parser( + parser: NSXMLParser, + didStartMappingPrefix: String, + toURI: String ) { curPrefixMapInverted.put(didStartMappingPrefix, toURI) curPrefixMap = curPrefixMapInverted.entries.associateBy({ it.value }, { it.key }) diff --git a/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/ComposeResource.skiko.kt b/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/ComposeResource.skiko.kt deleted file mode 100644 index 5400b51630..0000000000 --- a/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/ComposeResource.skiko.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package org.jetbrains.compose.resources - -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.toComposeImageBitmap -import org.jetbrains.skia.Image - -internal actual fun ByteArray.toImageBitmap(): ImageBitmap = - Image.makeFromEncoded(this).toComposeImageBitmap() 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 new file mode 100644 index 0000000000..0ac5f6f069 --- /dev/null +++ b/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt @@ -0,0 +1,43 @@ +package org.jetbrains.compose.resources + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.platform.Font +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +private val emptyFontBase64 = + "T1RUTwAJAIAAAwAQQ0ZGIML7MfIAAAQIAAAA2U9TLzJmMV8PAAABAAAAAGBjbWFwANUAVwAAA6QAAABEaGVhZCMuU7" + + "IAAACcAAAANmhoZWECvgAmAAAA1AAAACRobXR4Az4AAAAABOQAAAAQbWF4cAAEUAAAAAD4AAAABm5hbWUpw3nbAAABYAAAAkNwb3N0AAMA" + + "AAAAA+gAAAAgAAEAAAABAADs7nftXw889QADA+gAAAAA4WWJaQAAAADhZYlpAAAAAAFNAAAAAAADAAIAAAAAAAAAAQAAArz+1AAAAU0AAA" + + "AAAAAAAQAAAAAAAAAAAAAAAAAAAAQAAFAAAAQAAAADAHwB9AAFAAACigK7AAAAjAKKArsAAAHfADEBAgAAAAAAAAAAAAAAAAAAAAEAAAAA" + + "AAAAAAAAAABYWFhYAEAAIABfArz+1AAAAAAAAAAAAAEAAAAAAV4AAAAgACAAAAAAACIBngABAAAAAAAAAAIAbwABAAAAAAABAAUAAAABAA" + + "AAAAACAAcADwABAAAAAAADABAAdQABAAAAAAAEAA0AJAABAAAAAAAFAAIAbwABAAAAAAAGAAwASwABAAAAAAAHAAIAbwABAAAAAAAIAAIA" + + "bwABAAAAAAAJAAIAbwABAAAAAAAKAAIAbwABAAAAAAALAAIAbwABAAAAAAAMAAIAbwABAAAAAAANAAIAbwABAAAAAAAOAAIAbwABAAAAAA" + + "AQAAUAAAABAAAAAAARAAcADwADAAEECQAAAAQAcQADAAEECQABAAoABQADAAEECQACAA4AFgADAAEECQADACAAhQADAAEECQAEABoAMQAD" + + "AAEECQAFAAQAcQADAAEECQAGABgAVwADAAEECQAHAAQAcQADAAEECQAIAAQAcQADAAEECQAJAAQAcQADAAEECQAKAAQAcQADAAEECQALAA" + + "QAcQADAAEECQAMAAQAcQADAAEECQANAAQAcQADAAEECQAOAAQAcQADAAEECQAQAAoABQADAAEECQARAA4AFmVtcHR5AGUAbQBwAHQAeVJl" + + "Z3VsYXIAUgBlAGcAdQBsAGEAcmVtcHR5IFJlZ3VsYXIAZQBtAHAAdAB5ACAAUgBlAGcAdQBsAGEAcmVtcHR5UmVndWxhcgBlAG0AcAB0AH" + + "kAUgBlAGcAdQBsAGEAciIiACIAIiIiOmVtcHR5IFJlZ3VsYXIAIgAiADoAZQBtAHAAdAB5ACAAUgBlAGcAdQBsAGEAcgAAAAABAAMAAQAA" + + "AAwABAA4AAAACgAIAAIAAgAAACAAQQBf//8AAAAAACAAQQBf//8AAP/h/8H/pAABAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAQAEAQABAQENZW1wdHlSZWd1bGFyAAEBASf4GwD4HAL4HQP4HgSLi/lQ9+EFHQAAAHgPHQAAAH8Rix0AAADZEgAHAQED" + + "EBUcISIsIiJlbXB0eSBSZWd1bGFyZW1wdHlSZWd1bGFyc3BhY2VBdW5kZXJzY29yZQAAAAGLAYwBjQAEAQFMT1FT+F2f+TcVi4uL/TeLiw" + + "iLi/g1i4uLCIuLi/k3i4sIi4v8NYuLiwi7/QcVi4uL+NeLiwiLi/fUi4uLCIuLi/zXi4sIi4v71IuLiwgO9+EOnw6fDgAAAAHJAAABTQAA" + + "ABQAAAAUAAA=" + +@OptIn(ExperimentalEncodingApi::class) +private val defaultEmptyFont by lazy { Font("org.jetbrains.compose.emptyFont", Base64.decode(emptyFontBase64)) } + +@ExperimentalResourceApi +@Composable +actual fun Font(id: ResourceId, 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) + } + return fontFile +} \ No newline at end of file diff --git a/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/ImageResources.skiko.kt b/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/ImageResources.skiko.kt new file mode 100644 index 0000000000..be168978eb --- /dev/null +++ b/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/ImageResources.skiko.kt @@ -0,0 +1,8 @@ +package org.jetbrains.compose.resources + +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.toComposeImageBitmap +import org.jetbrains.skia.Image + +internal actual fun ByteArray.toImageBitmap(): ImageBitmap = + Image.makeFromEncoded(this).toComposeImageBitmap() \ No newline at end of file