Limitation: for a correct work on the android user is supposed to copy a font file to the android asset directorypull/3921/head
@ -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() |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -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() } |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -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() |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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, |
||||||
|
) |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -1,37 +1,65 @@ |
|||||||
package org.jetbrains.compose.resources.demo.shared |
package org.jetbrains.compose.resources.demo.shared |
||||||
|
|
||||||
import androidx.compose.foundation.Image |
import androidx.compose.foundation.layout.PaddingValues |
||||||
import androidx.compose.foundation.layout.* |
import androidx.compose.foundation.layout.fillMaxSize |
||||||
import androidx.compose.material.Icon |
import androidx.compose.material.icons.Icons |
||||||
import androidx.compose.material.Text |
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.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.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 |
@Composable |
||||||
internal fun UseResources() { |
internal fun UseResources() { |
||||||
Column { |
var screen by remember { mutableStateOf(Screens.Images) } |
||||||
Text("Hello, resources") |
|
||||||
Image( |
Scaffold( |
||||||
bitmap = resource("dir/img.png").rememberImageBitmap().orEmpty(), |
modifier = Modifier.fillMaxSize(), |
||||||
contentDescription = null, |
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") } |
||||||
) |
) |
||||||
Image( |
NavigationBarItem( |
||||||
bitmap = resource("img.webp").rememberImageBitmap().orEmpty(), |
selected = screen == Screens.Strings, |
||||||
contentDescription = null, |
onClick = { screen = Screens.Strings }, |
||||||
|
icon = { Icon(imageVector = Icons.Default.Abc, contentDescription = null) }, |
||||||
|
label = { Text("Strings") } |
||||||
) |
) |
||||||
Icon( |
NavigationBarItem( |
||||||
imageVector = resource("vector.xml").rememberImageVector(LocalDensity.current).orEmpty(), |
selected = screen == Screens.Font, |
||||||
modifier = Modifier.size(150.dp), |
onClick = { screen = Screens.Font }, |
||||||
contentDescription = null |
icon = { Icon(imageVector = Icons.Default.TextFields, contentDescription = null) }, |
||||||
|
label = { Text("Fonts") } |
||||||
) |
) |
||||||
Icon( |
NavigationBarItem( |
||||||
painter = painterResource("dir/vector.xml"), |
selected = screen == Screens.File, |
||||||
modifier = Modifier.size(150.dp), |
onClick = { screen = Screens.File }, |
||||||
contentDescription = null |
icon = { Icon(imageVector = Icons.Default.Attachment, contentDescription = null) }, |
||||||
|
label = { Text("Files") } |
||||||
) |
) |
||||||
} |
} |
||||||
|
} |
||||||
|
) |
||||||
} |
} |
||||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
@ -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> |
@ -1,8 +0,0 @@ |
|||||||
#root { |
|
||||||
width: 100%; |
|
||||||
height: 100vh; |
|
||||||
} |
|
||||||
|
|
||||||
#root > .compose-web-column > div { |
|
||||||
position: relative; |
|
||||||
} |
|
@ -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() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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) |
|
||||||
} |
|
||||||
} |
|
@ -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); |
|
||||||
} |
|
@ -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) |
||||||
|
} |
@ -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() |
@ -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") |
|
@ -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() } |
||||||
|
) |
||||||
|
} |
@ -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") |
@ -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) |
@ -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() } |
|
||||||
|
|
@ -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) |
|
@ -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 |
|
@ -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 |
@ -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)) } |
@ -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 |
|
||||||
} |
|
@ -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> |
@ -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() |
|
||||||
} |
|
||||||
} |
|
@ -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 |
@ -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 |
@ -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 } |
@ -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 } |
@ -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() |
||||||
|
} |
||||||
|
} |
@ -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) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
package org.jetbrains.compose.resources |
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope |
||||||
|
|
||||||
|
expect fun runBlockingTest(block: suspend CoroutineScope.() -> Unit) |
Before Width: | Height: | Size: 946 B After Width: | Height: | Size: 946 B |
Before Width: | Height: | Size: 785 B After Width: | Height: | Size: 785 B |
@ -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> |
@ -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") |
|
@ -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() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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) |
|
||||||
} |
|
||||||
} |
|
Before Width: | Height: | Size: 946 B |
Before Width: | Height: | Size: 785 B |
@ -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") |
|
@ -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) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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) |
||||||
|
} |
@ -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 |
||||||
|
} |
@ -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() |
||||||
|
} |
@ -1,9 +1,12 @@ |
|||||||
package org.jetbrains.compose.resources.vector.xmldom |
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.Element as DomElement |
||||||
|
import org.w3c.dom.Node as DomNode |
||||||
|
|
||||||
internal open class NodeImpl(val n: DomNode): Node { |
internal open class NodeImpl(val n: DomNode): Node { |
||||||
|
override val textContent: String? |
||||||
|
get() = n.textContent |
||||||
|
|
||||||
override val nodeName: String |
override val nodeName: String |
||||||
get() = n.nodeName |
get() = n.nodeName |
||||||
|
|
@ -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) |
@ -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 |
||||||
|
) |
@ -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) |
|
@ -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() |
||||||
|
} |
@ -1,9 +1,12 @@ |
|||||||
package org.jetbrains.compose.resources.vector.xmldom |
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.Element as DomElement |
||||||
|
import org.w3c.dom.Node as DomNode |
||||||
|
|
||||||
internal open class NodeImpl(val n: DomNode): Node { |
internal open class NodeImpl(val n: DomNode): Node { |
||||||
|
override val textContent: String? |
||||||
|
get() = n.textContent |
||||||
|
|
||||||
override val nodeName: String |
override val nodeName: String |
||||||
get() = n.nodeName |
get() = n.nodeName |
||||||
override val localName: String |
override val localName: String |
@ -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") |
|
||||||
|
|
@ -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) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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()) |
@ -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()) |
|
@ -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() |
|
@ -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 |
||||||
|
} |
@ -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() |