Browse Source

Support bitmap/vector images, strings, fonts and raw resource loading.

Limitation: for a correct work on the android user is supposed to copy a font file to the android asset directory
pull/3921/head
Konstantin 1 year ago committed by GitHub
parent
commit
5d1eb9a3f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 42
      components/AnimatedImage/demo/src/jvmMain/kotlin/org/jetbrains/compose/animatedimage/demo/Main.kt
  2. 1
      components/AnimatedImage/library/build.gradle.kts
  3. 1
      components/gradle.properties
  4. 10
      components/resources/demo/shared/build.gradle.kts
  5. BIN
      components/resources/demo/shared/src/androidMain/assets/font_awesome.otf
  6. 63
      components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt
  7. 54
      components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FontRes.kt
  8. 176
      components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/ImagesRes.kt
  9. 115
      components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt
  10. 86
      components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/UseResources.kt
  11. BIN
      components/resources/demo/shared/src/commonMain/resources/font_awesome.otf
  12. 0
      components/resources/demo/shared/src/commonMain/resources/images/compose.png
  13. 0
      components/resources/demo/shared/src/commonMain/resources/images/droid_icon.xml
  14. 0
      components/resources/demo/shared/src/commonMain/resources/images/insta_icon.xml
  15. 0
      components/resources/demo/shared/src/commonMain/resources/images/land.webp
  16. 13
      components/resources/demo/shared/src/commonMain/resources/strings.xml
  17. 15
      components/resources/demo/shared/src/desktopMain/kotlin/org/jetbrains/compose/resources/demo/shared/main.desktop.kt
  18. 18
      components/resources/demo/shared/src/iosMain/kotlin/main.ios.kt
  19. 28
      components/resources/demo/shared/src/jsMain/kotlin/main.js.kt
  20. 14
      components/resources/demo/shared/src/jsMain/resources/index.html
  21. 8
      components/resources/demo/shared/src/jsMain/resources/styles.css
  22. 68
      components/resources/library/build.gradle.kts
  23. BIN
      components/resources/library/src/androidInstrumentedTest/assets/font_awesome.otf
  24. 107
      components/resources/library/src/androidInstrumentedTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.android.kt
  25. 61
      components/resources/library/src/androidInstrumentedTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt
  26. 18
      components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ComposableResource.android.kt
  27. 15
      components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt
  28. 8
      components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ImageResources.android.kt
  29. 27
      components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/Resource.android.kt
  30. 14
      components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/PlatformState.blocking.kt
  31. 9
      components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/Resource.blocking.kt
  32. 6
      components/resources/library/src/blockingTest/kotlin/org/jetbrains/compose/resources/TestUtils.blocking.kt
  33. 14
      components/resources/library/src/commonButJSMain/kotlin/org/jetbrains/compose/resources/Resource.commonbutjs.kt
  34. 135
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ComposeResource.common.kt
  35. 9
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ExperimentalResourceApi.kt
  36. 25
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt
  37. 101
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt
  38. 114
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/LoadState.kt
  39. 16
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PlatformState.kt
  40. 38
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.common.kt
  41. 10
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.kt
  42. 14
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceIndex.kt
  43. 27
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt
  44. 153
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt
  45. 12
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/XmlVectorParser.kt
  46. 2
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/Node.kt
  47. 14
      components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/RecompositionsCounter.kt
  48. 51
      components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ResourceTest.kt
  49. 11
      components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestResourceReader.kt
  50. 5
      components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt
  51. 0
      components/resources/library/src/commonTest/resources/1.png
  52. 0
      components/resources/library/src/commonTest/resources/2.png
  53. BIN
      components/resources/library/src/commonTest/resources/font_awesome.otf
  54. 10
      components/resources/library/src/commonTest/resources/strings.xml
  55. 27
      components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/Resource.desktop.kt
  56. 107
      components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.desktop.kt
  57. 61
      components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt
  58. BIN
      components/resources/library/src/desktopTest/resources/1.png
  59. BIN
      components/resources/library/src/desktopTest/resources/2.png
  60. 38
      components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/Resource.ios.kt
  61. 20
      components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.ios.kt
  62. 13
      components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ImageResources.js.kt
  63. 16
      components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/PlatformState.js.kt
  64. 91
      components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt
  65. 19
      components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.js.kt
  66. 2
      components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/ElementImpl.kt
  67. 5
      components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/NodeImpl.kt
  68. 9
      components/resources/library/src/jsTest/kotlin/org/jetbrains/compose/resources/TestUtils.js.kt
  69. 15
      components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/ImageResources.jvmAndAndroid.kt
  70. 22
      components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/Resource.jvmandandroid.kt
  71. 10
      components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.jvmAndAndroid.kt
  72. 0
      components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/ElementImpl.kt
  73. 5
      components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/NodeImpl.kt
  74. 40
      components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/Resource.macos.kt
  75. 21
      components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.macos.kt
  76. 6
      components/resources/library/src/nativeMain/kotlin/org/jetbrains/compose/resources/ImageResources.native.kt
  77. 11
      components/resources/library/src/nativeMain/kotlin/org/jetbrains/compose/resources/Resource.native.kt
  78. 53
      components/resources/library/src/nativeMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt
  79. 13
      components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/ComposeResource.skiko.kt
  80. 43
      components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt
  81. 8
      components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/ImageResources.skiko.kt

42
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<T> {
class Loading<T> : LoadState<T>
data class Success<T>(val data: T) : LoadState<T>
data class Error<T>(val error: Exception) : LoadState<T>
}
@Composable
private fun <T> 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<AnimatedImage> = 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!")
}

1
components/AnimatedImage/library/build.gradle.kts

@ -16,7 +16,6 @@ kotlin {
dependencies {
api(compose.runtime)
api(compose.foundation)
api(project(":resources:library"))
}
}
}

1
components/gradle.properties

@ -24,3 +24,4 @@ kotlin.js.compiler=ir
kotlin.js.webpack.major.version=4
kotlin.native.useEmbeddableCompilerJar=true
kotlin.native.binary.memoryModel=experimental
xcodeproj=./resources/demo/iosApp

10
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"))
}
}

BIN
components/resources/demo/shared/src/androidMain/assets/font_awesome.otf

Binary file not shown.

63
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()
)
}
}

54
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() }
)
}
}

176
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()
)
}
}
}
}

115
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,
)
)
}
}

86
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") }
)
}
}
)
}

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

Binary file not shown.

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

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

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

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

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

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

13
components/resources/demo/shared/src/commonMain/resources/strings.xml

@ -0,0 +1,13 @@
<resources>
<string name="app_name">Compose Resources App</string>
<string name="hello">😊 Hello world!</string>
<string name="multi_line">Lorem ipsum dolor sit amet,
consectetur adipiscing elit.
Donec eget turpis ac sem ultricies consequat.</string>
<string name="str_template">Hello, %1$s! You have %2$d new messages.</string>
<string-array name="str_arr">
<item>item 1</item>
<item>item 2</item>
<item>item 3</item>
</string-array>
</resources>

15
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())
}

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

28
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()
}
}

14
components/resources/demo/shared/src/jsMain/resources/index.html

@ -2,15 +2,13 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>compose multiplatform web demo</title>
<script src="skiko.js"> </script>
<link type="text/css" rel="stylesheet" href="styles.css" />
<title>compose resources web demo</title>
<script src="skiko.js"></script>
</head>
<body>
<h1>compose multiplatform web demo</h1>
<div>
<canvas id="ComposeTarget" width="800" height="600"></canvas>
</div>
<script src="shared.js"> </script>
<div>
<canvas id="ComposeTarget" width="800" height="600"></canvas>
</div>
<script src="shared.js"></script>
</body>
</html>

8
components/resources/demo/shared/src/jsMain/resources/styles.css

@ -1,8 +0,0 @@
#root {
width: 100%;
height: 100vh;
}
#root > .compose-web-column > div {
position: relative;
}

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

BIN
components/resources/library/src/androidInstrumentedTest/assets/font_awesome.otf

Binary file not shown.

107
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()
}
}
}

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

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

18
components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ComposableResource.android.kt

@ -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);
}

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

8
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()

27
components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/Resource.android.kt

@ -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")

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

9
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")

6
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)

14
components/resources/library/src/commonButJSMain/kotlin/org/jetbrains/compose/resources/Resource.commonbutjs.kt

@ -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() }

135
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ComposeResource.common.kt

@ -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<ImageBitmap> {
val state: MutableState<LoadState<ImageBitmap>> = 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<ImageVector> {
val state: MutableState<LoadState<ImageVector>> = 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 <T> LoadState<T>.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<ImageBitmap>.orEmpty(): ImageBitmap = orEmpty(emptyImageBitmap)
/**
* Return current ImageVector or return empty while loading.
*/
@ExperimentalResourceApi
fun LoadState<ImageVector>.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)

9
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ExperimentalResourceApi.kt

@ -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

25
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

101
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<String, ImageCache>()
//@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)) }

114
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/LoadState.kt

@ -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<T> {
class Loading<T> : LoadState<T>()
data class Success<T>(val value: T) : LoadState<T>()
data class Error<T>(val exception: Exception) : LoadState<T>()
}
/**
* 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 <T> load(load: suspend () -> T): LoadState<T> {
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 <T: Any> 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 <T> load(key1: Any?, load: suspend () -> T): LoadState<T> {
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 <T: Any> 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 <T> load(key1: Any?, key2: Any?, load: suspend () -> T): LoadState<T> {
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 <T: Any> 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 <T> load(key1: Any?, key2: Any?, key3: Any?, load: suspend () -> T): LoadState<T> {
var state: LoadState<T> 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 <T: Any> loadOrNull(key1: Any?, key2: Any?, key3: Any?, load: suspend () -> T): T? {
val state = load(key1, key2, key3, load)
return (state as? LoadState.Success<T>)?.value
}

16
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 <T> rememberState(
key: Any,
getDefault: () -> T,
block: suspend () -> T
): State<T>

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

@ -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 <sourceSet>/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()
}
}

10
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

14
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<String> =
rememberState(id, { "" }) { getPathById(id) }
internal val ResourceId.stringKey get() = this

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

153
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<String>) : StringItem
}
private val parsedStringsCache = mutableMapOf<String, Map<String, StringItem>>()
//@TestOnly
internal fun dropStringsCache() {
parsedStringsCache.clear()
}
private suspend fun getParsedStrings(path: String, resourceReader: ResourceReader): Map<String, StringItem> =
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<String>, 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<String> {
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<String> = loadStringArray(id, DefaultResourceReader)
private suspend fun loadStringArray(id: ResourceId, resourceReader: ResourceReader): List<String> {
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<Element> =
List(length) { item(it) }
.filterIsInstance<Element>()
.filter { it.localName == name }

12
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(

2
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

14
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()
}
}

51
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<MissingResourceException> {
readResourceBytes("missing.png")
}
val error = assertFailsWith<IllegalStateException> {
loadString("unknown_id")
}
assertEquals("String ID=`unknown_id` is not found!", error.message)
}
@Test
fun testReadFileResource() = runBlockingTest {
val bytes = readResourceBytes("strings.xml")
assertEquals(
"""
<resources>
<string name="app_name">Compose Resources App</string>
<string name="hello">😊 Hello world!</string>
<string name="str_template">Hello, %1${'$'}s! You have %2${'$'}d new messages.</string>
<string-array name="str_arr">
<item>item 1</item>
<item>item 2</item>
<item>item 3</item>
</string-array>
</resources>
""".trimIndent(),
bytes.decodeToString()
)
}
@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"))
}
}

11
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<String>()
val readPaths: List<String> get() = readPathsList
override suspend fun read(path: String): ByteArray {
readPathsList.add(path)
return DefaultResourceReader.read(path)
}
}

5
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)

0
components/resources/library/src/androidInstrumentedTest/resources/1.png → components/resources/library/src/commonTest/resources/1.png

Before

Width:  |  Height:  |  Size: 946 B

After

Width:  |  Height:  |  Size: 946 B

0
components/resources/library/src/androidInstrumentedTest/resources/2.png → components/resources/library/src/commonTest/resources/2.png

Before

Width:  |  Height:  |  Size: 785 B

After

Width:  |  Height:  |  Size: 785 B

BIN
components/resources/library/src/commonTest/resources/font_awesome.otf

Binary file not shown.

10
components/resources/library/src/commonTest/resources/strings.xml

@ -0,0 +1,10 @@
<resources>
<string name="app_name">Compose Resources App</string>
<string name="hello">😊 Hello world!</string>
<string name="str_template">Hello, %1$s! You have %2$d new messages.</string>
<string-array name="str_arr">
<item>item 1</item>
<item>item 2</item>
<item>item 3</item>
</string-array>
</resources>

27
components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/Resource.desktop.kt

@ -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")

107
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()
}
}
}

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

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

BIN
components/resources/library/src/desktopTest/resources/1.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 946 B

BIN
components/resources/library/src/desktopTest/resources/2.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 785 B

38
components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/Resource.ios.kt

@ -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")

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

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

16
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 <T> rememberState(key: Any, getDefault: () -> T, block: suspend () -> T): State<T> {
val state = remember(key) { mutableStateOf(getDefault()) }
LaunchedEffect(key) {
state.value = block()
}
return state
}

91
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" }
* }
* ```
*/
@ -66,52 +50,3 @@ object WebResourcesConfiguration {
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<ByteArray>()
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()

19
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<ByteArray>()
@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()
}

2
components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/xmldom/ElementImpl.kt → 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

5
components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/xmldom/NodeImpl.kt → 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

9
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)

15
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
)

22
components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/Resource.jvmandandroid.kt

@ -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)

10
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()
}

0
components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/xmldom/ElementImpl.kt → components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/ElementImpl.kt

5
components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/xmldom/NodeImpl.kt → 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

40
components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/Resource.macos.kt

@ -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")

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

6
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())

11
components/resources/library/src/nativeMain/kotlin/org/jetbrains/compose/resources/Resource.native.kt

@ -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())

53
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<String, String>,
val attributes: Map<Any?, *>): Element {
private class ElementImpl(
override val localName: String,
override val nodeName: String,
override val namespaceURI: String,
val prefixMap: Map<String, String>,
val attributes: Map<Any?, *>
) : 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<String, String>()
@ -68,8 +75,13 @@ private class DomXmlParser(
qualifiedName: String?,
attributes: Map<Any?, *>
) {
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 })

13
components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/ComposeResource.skiko.kt

@ -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()

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

8
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()
Loading…
Cancel
Save