Skip to content

Commit

Permalink
Get environment and select resource by qualifiers
Browse files Browse the repository at this point in the history
  • Loading branch information
terrakok committed Dec 8, 2023
1 parent dfaeced commit e88fa3d
Show file tree
Hide file tree
Showing 20 changed files with 422 additions and 86 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package org.jetbrains.compose.resources

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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
import androidx.compose.ui.text.font.*

@ExperimentalResourceApi
@Composable
actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): Font {
val path = resource.getPathByEnvironment()
val environment = rememberEnvironment()
val path = remember(environment) { resource.getPathByEnvironment(environment) }
return Font(path, LocalContext.current.assets, weight, style)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.jetbrains.compose.resources

import android.content.res.Configuration
import android.content.res.Resources
import java.util.*

internal actual fun getResourceEnvironment(): ResourceEnvironment {
val locale = Locale.getDefault()
val configuration = Resources.getSystem().configuration
val isDarkTheme = configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
val dpi = configuration.densityDpi
return ResourceEnvironment(
language = LanguageQualifier(locale.language),
region = RegionQualifier(locale.country),
theme = ThemeQualifier.selectByValue(isDarkTheme),
density = DensityQualifier.selectByValue(dpi)
)
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.jetbrains.compose.resources

import androidx.compose.runtime.*
import kotlinx.coroutines.runBlocking

@Composable
internal actual fun <T> rememberResourceState(
key: Any,
getDefault: () -> T,
block: suspend (ResourceEnvironment) -> T
): State<T> {
val environment = rememberEnvironment()
return remember(key, environment) {
mutableStateOf(
runBlocking { block(environment) }
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ package org.jetbrains.compose.resources

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.*

/**
* Represents a font resource.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
package org.jetbrains.compose.resources

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.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.dp
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.jetbrains.compose.resources.vector.toImageVector
Expand Down Expand Up @@ -50,7 +44,8 @@ fun ImageResource(path: String): ImageResource = ImageResource(
@ExperimentalResourceApi
@Composable
fun painterResource(resource: ImageResource): Painter {
val filePath = remember(resource) { resource.getPathByEnvironment() }
val environment = rememberEnvironment()
val filePath = remember(resource, environment) { resource.getPathByEnvironment(environment) }
val isXml = filePath.endsWith(".xml", true)
if (isXml) {
return rememberVectorPainter(vectorResource(resource))
Expand All @@ -71,8 +66,8 @@ private val emptyImageBitmap: ImageBitmap by lazy { ImageBitmap(1, 1) }
@Composable
fun imageResource(resource: ImageResource): ImageBitmap {
val resourceReader = LocalResourceReader.current
val imageBitmap by rememberState(resource, { emptyImageBitmap }) {
val path = resource.getPathByEnvironment()
val imageBitmap by rememberResourceState(resource, { emptyImageBitmap }) { env ->
val path = resource.getPathByEnvironment(env)
val cached = loadImage(path, resourceReader) {
ImageCache.Bitmap(it.toImageBitmap())
} as ImageCache.Bitmap
Expand All @@ -96,8 +91,8 @@ private val emptyImageVector: ImageVector by lazy {
fun vectorResource(resource: ImageResource): ImageVector {
val resourceReader = LocalResourceReader.current
val density = LocalDensity.current
val imageVector by rememberState(resource, { emptyImageVector }) {
val path = resource.getPathByEnvironment()
val imageVector by rememberResourceState(resource, { emptyImageVector }) { env ->
val path = resource.getPathByEnvironment(env)
val cached = loadImage(path, resourceReader) {
ImageCache.Vector(it.toXmlElement().toImageVector(density))
} as ImageCache.Vector
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package org.jetbrains.compose.resources

interface Qualifier

data class LanguageQualifier(
val language: String
) : Qualifier {
companion object {
val regex = Regex("[a-z][a-z]")
}
}

data class RegionQualifier(
val region: String
) : Qualifier {
companion object {
val regex = Regex("r[A-Z][A-Z]")
}
}

enum class ThemeQualifier(val code: String) : Qualifier {
LIGHT("light"),
DARK("dark");

companion object {
fun selectByValue(isDark: Boolean) =
if (isDark) DARK else LIGHT
}
}

//https://developer.android.com/guide/topics/resources/providing-resources
enum class DensityQualifier(val code: String, val dpi: Int) : Qualifier {
LDPI("ldpi", 120),
MDPI("mdpi", 160),
HDPI("hdpi", 240),
XHDPI("xhdpi", 320),
XXHDPI("xxhdpi", 480),
XXXHDPI("xxxhdpi", 640);

companion object {
fun selectByValue(dpi: Int) = when {
dpi <= LDPI.dpi -> LDPI
dpi <= MDPI.dpi -> MDPI
dpi <= HDPI.dpi -> HDPI
dpi <= XHDPI.dpi -> XHDPI
dpi <= XXHDPI.dpi -> XXHDPI
else -> XXXHDPI
}
fun selectByDensity(density: Float) = when {
density <= 0.75 -> LDPI
density <= 1.0 -> MDPI
density <= 1.33 -> HDPI
density <= 2.0 -> XHDPI
density <= 3.0 -> XXHDPI
else -> XXXHDPI
}
}
}

//TODO: move it to the gradle plugin
internal fun List<String>.parseQualifiers(): List<Qualifier> {
var language: LanguageQualifier? = null
var region: RegionQualifier? = null
var theme: ThemeQualifier? = null
var density: DensityQualifier? = null

this.forEach { q ->
if (density == null) {
DensityQualifier.entries.firstOrNull { it.code == q }?.let {
density = it
return@forEach
}
}
if (theme == null) {
ThemeQualifier.entries.firstOrNull { it.code == q }?.let {
theme = it
return@forEach
}
}
if (language == null && q.matches(LanguageQualifier.regex)) {
language = LanguageQualifier(q)
return@forEach
}
if (region == null && q.matches(RegionQualifier.regex)) {
region = RegionQualifier(q.takeLast(2))
return@forEach
}
}

return buildList {
language?.let { add(it) }
region?.let { add(it) }
theme?.let { add(it) }
density?.let { add(it) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,3 @@ data class ResourceItem(
internal val qualifiers: Set<String>,
internal val path: String
)

internal fun Resource.getPathByEnvironment(): String {
//TODO
return items.first().path
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.jetbrains.compose.resources

import androidx.compose.runtime.*
import androidx.compose.ui.LocalSystemTheme
import androidx.compose.ui.SystemTheme
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.intl.Locale

internal data class ResourceEnvironment(
val language: LanguageQualifier,
val region: RegionQualifier,
val theme: ThemeQualifier,
val density: DensityQualifier
)

@OptIn(InternalComposeApi::class)
@Composable
internal fun rememberEnvironment(): ResourceEnvironment {
val composeLocale = Locale.current
val composeTheme = LocalSystemTheme.current
val composeDensity = LocalDensity.current

//cache ResourceEnvironment unless compose environment is changed
//TODO provide top level function with a single cache in a root of compose tree
return remember(composeLocale, composeTheme, composeDensity) {
ResourceEnvironment(
LanguageQualifier(composeLocale.language),
RegionQualifier(composeLocale.region),
ThemeQualifier.selectByValue(composeTheme == SystemTheme.Dark),
DensityQualifier.selectByDensity(composeDensity.density)
)
}
}

//expensive operation - do not use during recomposition
//it is required for a non-composable access to string resources
internal expect fun getResourceEnvironment(): ResourceEnvironment

internal fun Resource.getPathByEnvironment(environment: ResourceEnvironment): String {
items.toList()
.filterBy(environment.language)
.also { if (it.size == 1) return it.first().path }
.filterBy(environment.region)
.also { if (it.size == 1) return it.first().path }
.filterBy(environment.theme)
.also { if (it.size == 1) return it.first().path }
.filterBy(environment.density)
.also { if (it.size == 1) return it.first().path }
.let { return it.first().path }
}

private fun List<ResourceItem>.filterBy(qualifier: Qualifier): List<ResourceItem> {
val items = map { it to it.qualifiers.toList().parseQualifiers() }
val withQualifier = items.filter { (_, qualifiers) ->
qualifiers.any { it == qualifier }
}.map { (item, _) -> item }

if (withQualifier.isNotEmpty()) return withQualifier

val withoutQualifier = items.filter { (_, qualifiers) ->
qualifiers.none { it::class == qualifier::class }
}.map { (item, _) -> item }

if (withoutQualifier.isNotEmpty()) return withoutQualifier

return this
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import androidx.compose.runtime.State
* On the JS platform it loads the state asynchronously and uses `getDefault` as an initial state value.
*/
@Composable
internal expect fun <T> rememberState(
internal expect fun <T> rememberResourceState(
key: Any,
getDefault: () -> T,
block: suspend () -> T
block: suspend (ResourceEnvironment) -> T
): State<T>
Loading

0 comments on commit e88fa3d

Please sign in to comment.