Skip to content

Commit

Permalink
Add FontVariation.Settings support to the resources library (#5183)
Browse files Browse the repository at this point in the history
Sample:
```kotlin
Text(
    modifier = Modifier.padding(16.dp),
    fontFamily = FontFamily(
        Font(
            Res.font.RobotoFlex_VariableFont,
            variationSettings = FontVariation.Settings(FontVariation.weight(400))
        ),
    ),
    text = "The quick brown fox jumps over the lazy dog"
)
```

Fixes https://youtrack.jetbrains.com/issue/CMP-7088

## Testing
Demo app:
`./gradlew :resources:demo:desktopApp:run`
<img width="600" alt="image"
src="https://github.com/user-attachments/assets/7fc0403b-6732-42bb-97dc-211136a82d44"
/>


## Release Notes
### Highlights - Resources
- Add FontVariation.Settings support to the resources library

Co-authored-by: adamglin <[email protected]>
  • Loading branch information
terrakok and adamglin0 authored Dec 13, 2024
1 parent 3ecade1 commit 93b2f1d
Show file tree
Hide file tree
Showing 12 changed files with 249 additions and 7 deletions.
2 changes: 1 addition & 1 deletion components/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ android.useAndroidX=true
#Versions
kotlin.version=1.9.24
agp.version=8.2.2
compose.version=1.7.0
compose.version=1.8.0-alpha01
deploy.version=0.1.0-SNAPSHOT

#Compose
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,33 @@ package org.jetbrains.compose.resources.demo.shared

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontVariation
import androidx.compose.ui.unit.dp
import components.resources.demo.shared.generated.resources.Res
import components.resources.demo.shared.generated.resources.*
import components.resources.demo.shared.generated.resources.RobotoFlex_VariableFont
import components.resources.demo.shared.generated.resources.Workbench_Regular
import components.resources.demo.shared.generated.resources.font_awesome
import org.jetbrains.compose.resources.Font
import kotlin.math.roundToInt

@Composable
fun FontRes(paddingValues: PaddingValues) {
Expand Down Expand Up @@ -74,5 +87,56 @@ fun FontRes(paddingValues: PaddingValues) {
style = MaterialTheme.typography.headlineLarge,
text = "\uf1ba \uf238 \uf21a \uf1bb \uf1b8 \uf09b \uf269 \uf1d0 \uf15a \uf293 \uf1c6"
)

var variableFontWeight by remember { mutableStateOf(200) }
val variableFont = Font(
resource = Res.font.RobotoFlex_VariableFont,
variationSettings = FontVariation.Settings(FontVariation.weight(variableFontWeight))
)
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 = """
Text(
modifier = Modifier.padding(16.dp),
fontFamily = FontFamily(
Font(
Res.font.RobotoFlex_VariableFont,
variationSettings = FontVariation.Settings(
FontVariation.weight($variableFontWeight)
)
),
),
text = "The quick brown fox jumps over the lazy dog"
)
""".trimIndent(),
color = MaterialTheme.colorScheme.onPrimaryContainer,
softWrap = false
)
}

Text(
modifier = Modifier.padding(16.dp),
fontFamily = FontFamily(variableFont),
text = "The quick brown fox jumps over the lazy dog"
)

Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 16.dp)
) {
Text(text = "Weight:")
Spacer(modifier = Modifier.size(16.dp))
Slider(
value = variableFontWeight.toFloat(),
onValueChange = { variableFontWeight = it.roundToInt() },
valueRange = 100f..1000f,
steps = 9
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,27 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.*

@Deprecated(
message = "Use the new Font function with variationSettings instead.",
level = DeprecationLevel.HIDDEN
)
@Composable
actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): Font {
val environment = LocalComposeEnvironment.current.rememberEnvironment()
val path = remember(environment, resource) { resource.getResourceItemByEnvironment(environment).path }
val assets = LocalContext.current.assets
return Font(path, assets, weight, style)
}

@Composable
actual fun Font(
resource: FontResource,
weight: FontWeight,
style: FontStyle,
variationSettings: FontVariation.Settings,
): Font {
val environment = LocalComposeEnvironment.current.rememberEnvironment()
val path = remember(environment, resource) { resource.getResourceItemByEnvironment(environment).path }
val assets = LocalContext.current.assets
return Font(path, assets, weight, style, variationSettings)
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,21 @@ internal actual fun <T> rememberResourceState(
runBlocking { block(environment) }
)
}
}

@Composable
internal actual fun <T> rememberResourceState(
key1: Any,
key2: Any,
key3: Any,
key4: Any,
getDefault: () -> T,
block: suspend (ResourceEnvironment) -> T
): State<T> {
val environment = LocalComposeEnvironment.current.rememberEnvironment()
return remember(key1, key2, key3, key4, environment) {
mutableStateOf(
runBlocking { block(environment) }
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import androidx.compose.ui.text.font.*
*/
@Immutable
class FontResource
@InternalResourceApi constructor(id: String, items: Set<ResourceItem>): Resource(id, items)
@InternalResourceApi constructor(id: String, items: Set<ResourceItem>) : Resource(id, items)

/**
* Creates a font using the specified font resource, weight, and style.
Expand All @@ -28,13 +28,37 @@ class FontResource
*
* @throws NotFoundException if the specified resource ID is not found.
*/
@Deprecated(
message = "Use the new Font function with variationSettings instead.",
level = DeprecationLevel.HIDDEN
)
@Composable
expect fun Font(
resource: FontResource,
weight: FontWeight = FontWeight.Normal,
style: FontStyle = FontStyle.Normal
): Font

/**
* Creates a font using the specified font resource, weight, and style.
*
* @param resource The font resource to be used.
* @param weight The weight of the font. Default value is [FontWeight.Normal].
* @param style The style of the font. Default value is [FontStyle.Normal].
* @param variationSettings Custom variation settings for the font, with a default value derived from the specified [weight] and [style].
*
* @return The created [Font] object.
*
* @throws NotFoundException if the specified resource ID is not found.
*/
@Composable
expect fun Font(
resource: FontResource,
weight: FontWeight = FontWeight.Normal,
style: FontStyle = FontStyle.Normal,
variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style)
): Font

/**
* Retrieves the byte array of the font resource.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,20 @@ internal expect fun <T> rememberResourceState(
key3: Any,
getDefault: () -> T,
block: suspend (ResourceEnvironment) -> T
): State<T>


/**
* 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> rememberResourceState(
key1: Any,
key2: Any,
key3: Any,
key4: Any,
getDefault: () -> T,
block: suspend (ResourceEnvironment) -> T
): State<T>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.platform.Font
import androidx.compose.ui.unit.Density
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi

Expand Down Expand Up @@ -33,6 +34,10 @@ private val fontCache = AsyncCache<String, Font>()
internal val Font.isEmptyPlaceholder: Boolean
get() = this == defaultEmptyFont

@Deprecated(
message = "Use the new Font function with variationSettings instead.",
level = DeprecationLevel.HIDDEN
)
@Composable
actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): Font {
val resourceReader = LocalResourceReader.currentOrPreview
Expand All @@ -45,4 +50,31 @@ actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): F
}
}
return fontFile
}

@Composable
actual fun Font(
resource: FontResource,
weight: FontWeight,
style: FontStyle,
variationSettings: FontVariation.Settings,
): Font {
val resourceReader = LocalResourceReader.currentOrPreview
val fontFile by rememberResourceState(resource, weight, style, variationSettings, { defaultEmptyFont }) { env ->
val path = resource.getResourceItemByEnvironment(env).path
val key = "$path:$weight:$style:${variationSettings.getCacheKey()}"
fontCache.getOrLoad(key) {
val fontBytes = resourceReader.read(path)
Font(key, fontBytes, weight, style, variationSettings)
}
}
return fontFile
}

internal fun FontVariation.Settings.getCacheKey(): String {
val defaultDensity = Density(1f)
return settings
.map { "${it::class.simpleName}(${it.axisName},${it.toVariationValue(defaultDensity)})" }
.sorted()
.joinToString(",")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.jetbrains.compose.resources

import androidx.compose.ui.text.font.FontVariation
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class VariationFontCacheTest {

@Test
fun `getCacheKey should return an empty string for an empty settings list`() {
val settings = FontVariation.Settings()
val cacheKey = settings.getCacheKey()
assertEquals("", cacheKey, "Cache key for empty settings list should be an empty string")
}

@Test
fun `getCacheKey should return a correct key for a single setting`() {
val setting = FontVariation.Setting("wght", 700f)
val settings = FontVariation.Settings(setting)
val cacheKey = settings.getCacheKey()
assertEquals("SettingFloat(wght,700.0)", cacheKey, "Cache key for a single setting is incorrect")
}

@Test
fun `getCacheKey should correctly sort settings by class name and axis name`() {
val setting1 = FontVariation.Setting("wght", 400f)
val setting2 = FontVariation.Setting("ital", 1f)
val settings = FontVariation.Settings(setting1, setting2)
val cacheKey = settings.getCacheKey()
assertEquals(
"SettingFloat(ital,1.0),SettingFloat(wght,400.0)",
cacheKey,
"Cache key should sort settings by class name and axis name"
)
}

@Test
fun `getCacheKey should throw an exception when there are duplicate settings`() {
val setting1 = FontVariation.Setting("wght", 400f)
val setting2 = FontVariation.Setting("wght", 700f)

assertFailsWith<IllegalArgumentException>(
"'axis' must be unique"
) {
FontVariation.Settings(setting1, setting2)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontVariation
import androidx.compose.ui.text.font.FontWeight

/**
Expand Down Expand Up @@ -90,6 +91,7 @@ internal fun getResourceUrl(windowOrigin: String, windowPathname: String, resour
* @param resource The font resource to be used.
* @param weight The weight of the font. Default value is [FontWeight.Normal].
* @param style The style of the font. Default value is [FontStyle.Normal].
* @param variationSettings Custom variation settings for the font, with a default value derived from the specified [weight] and [style].
* @return A [State]<[Font]?> object that holds the loaded [Font] when available,
* or `null` if the font is not yet ready.
*/
Expand All @@ -98,10 +100,11 @@ internal fun getResourceUrl(windowOrigin: String, windowPathname: String, resour
fun preloadFont(
resource: FontResource,
weight: FontWeight = FontWeight.Normal,
style: FontStyle = FontStyle.Normal
style: FontStyle = FontStyle.Normal,
variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style),
): State<Font?> {
val resState = remember(resource, weight, style) { mutableStateOf<Font?>(null) }.apply {
value = Font(resource, weight, style).takeIf { !it.isEmptyPlaceholder }
val resState = remember(resource, weight, style, variationSettings) { mutableStateOf<Font?>(null) }.apply {
value = Font(resource, weight, style, variationSettings).takeIf { !it.isEmptyPlaceholder }
}
return resState
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,24 @@ internal actual fun <T> rememberResourceState(
}
mutableState
}
}

@Composable
internal actual fun <T> rememberResourceState(
key1: Any,
key2: Any,
key3: Any,
key4: Any,
getDefault: () -> T,
block: suspend (ResourceEnvironment) -> T
): State<T> {
val environment = LocalComposeEnvironment.current.rememberEnvironment()
val scope = rememberCoroutineScope()
return remember(key1, key2, key3, key4, environment) {
val mutableState = mutableStateOf(getDefault())
scope.launch(start = CoroutineStart.UNDISPATCHED) {
mutableState.value = block(environment)
}
mutableState
}
}
2 changes: 1 addition & 1 deletion gradle-plugins/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ kotlin.code.style=official
dev.junit.parallel=false

# Default version of Compose Libraries used by Gradle plugin
compose.version=1.7.1
compose.version=1.8.0-alpha01
# The latest version of Kotlin compatible with compose.tests.compiler.version. Used only in tests/CI.
compose.tests.kotlin.version=2.0.0
# __SUPPORTED_GRADLE_VERSIONS__
Expand Down

0 comments on commit 93b2f1d

Please sign in to comment.