Add settings for variable font axes in typography scheme

This commit is contained in:
MM20
2026-01-31 23:21:55 +01:00
parent 77bcd806c7
commit 24406f6aae
7 changed files with 173 additions and 52 deletions

View File

@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.navigationBarsPadding
@@ -29,6 +30,7 @@ import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LeadingIconTab
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedIconToggleButton
@@ -46,6 +48,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -72,6 +75,7 @@ import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.SliderPreference
import de.mm20.launcher2.ui.theme.typography.fontFamilyOf
import de.mm20.launcher2.ui.theme.typography.typographyOf
import kotlinx.serialization.Serializable
@@ -745,24 +749,60 @@ private fun FontPreference(
val fontManager = FontManager(context)
var showDialog by remember { mutableStateOf(false) }
var showFontSettings by remember { mutableStateOf(false) }
Preference(
title = title,
summary = getFontName(context, value),
icon = {
Text(
text = preview.ExtraShort,
style = TextStyle(
fontFamily = remember(value) { fontFamilyOf(context, value) },
fontWeight = FontWeight.Normal,
color = MaterialTheme.colorScheme.primary,
fontSize = 24.sp,
textAlign = TextAlign.Center,
)
val fontVariantAxes = remember(value) {
if (value is ThemeFontFamily.VariableFontFamily) {
fontManager.getFontSettings(value)
} else {
emptyList()
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.background(
MaterialTheme.colorScheme.surface,
MaterialTheme.shapes.extraSmall
)
) {
Box(
modifier = Modifier.weight(1f)
) {
Preference(
title = title,
summary = getFontName(context, value),
icon = {
Text(
text = preview.ExtraShort,
style = TextStyle(
fontFamily = remember(value) { fontFamilyOf(context, value) },
fontWeight = FontWeight.Normal,
color = MaterialTheme.colorScheme.primary,
fontSize = 24.sp,
textAlign = TextAlign.Center,
)
)
},
onClick = { showDialog = true },
containerColor = Color.Transparent,
)
},
onClick = { showDialog = true },
)
}
if (fontVariantAxes.isNotEmpty()) {
Box(
modifier = Modifier
.height(36.dp)
.width(1.dp)
.background(MaterialTheme.colorScheme.outlineVariant)
)
IconButton(
modifier = Modifier.padding(horizontal = 12.dp),
onClick = { showFontSettings = true }
) {
Icon(painterResource(R.drawable.tune_24px), null)
}
}
}
DismissableBottomSheet(
expanded = showDialog,
@@ -774,7 +814,8 @@ private fun FontPreference(
start = 16.dp,
top = 16.dp,
end = 16.dp,
bottom = 16.dp + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding(),
bottom = 16.dp + WindowInsets.navigationBars.asPaddingValues()
.calculateBottomPadding(),
),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
@@ -816,6 +857,59 @@ private fun FontPreference(
}
}
}
if (value is ThemeFontFamily.VariableFontFamily) {
DismissableBottomSheet(
expanded = showFontSettings,
onDismissRequest = {
showFontSettings = false
}
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(16.dp)
.navigationBarsPadding()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
.heightIn(min = 200.dp),
contentAlignment = Alignment.Center,
) {
Text(
modifier = Modifier.fillMaxWidth(),
text = preview.TwoLines,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.headlineMedium,
fontFamily = fontFamilyOf(context, value)
)
}
PreferenceCategory {
for (axis in fontVariantAxes) {
SliderPreference(
title = axis.label,
value = value.settings.get(axis.name) ?: axis.defaultValue,
onValueChanged = {
val map = value.settings.toMutableMap()
map[axis.name] = it
if (value is ThemeFontFamily.LauncherDefault) {
onValueChange(value.copy(settings = map))
}
},
min = axis.range.start,
max = axis.range.endInclusive,
step = axis.step,
)
}
}
}
}
}
}
@Composable
@@ -860,7 +954,6 @@ private fun FontPickerCategory(
private fun getFontName(context: Context, fontFamily: ThemeFontFamily?): String {
return when (fontFamily) {
is ThemeFontFamily.LauncherDefault -> "Google Sans Flex"
is ThemeFontFamily.LauncherDefaultRound -> "Google Sans Flex (Rounded)"
is ThemeFontFamily.DeviceHeadline -> context.getString(R.string.font_name_device_headline)
is ThemeFontFamily.DeviceBody -> context.getString(R.string.font_name_device_body)
is ThemeFontFamily.System -> fontFamily.name

View File

@@ -13,7 +13,6 @@ import androidx.compose.ui.unit.sp
import de.mm20.launcher2.themes.typography.DefaultEmphasizedTextStyles
import de.mm20.launcher2.themes.typography.DefaultTextStyles
import de.mm20.launcher2.ui.theme.typography.fontfamily.GoogleSansFlex
import de.mm20.launcher2.ui.theme.typography.fontfamily.GoogleSansFlexRound
import de.mm20.launcher2.ui.theme.typography.fontfamily.getDeviceBodyFontFamily
import de.mm20.launcher2.ui.theme.typography.fontfamily.getDeviceHeadlineFontFamily
import de.mm20.launcher2.themes.typography.FontFamily as ThemeFontFamily
@@ -317,8 +316,7 @@ fun fontFamilyOf(
fontFamily: ThemeFontFamily?
): FontFamily {
return when (fontFamily) {
is ThemeFontFamily.LauncherDefault -> GoogleSansFlex
is ThemeFontFamily.LauncherDefaultRound -> GoogleSansFlexRound
is ThemeFontFamily.LauncherDefault -> GoogleSansFlex(fontFamily.settings)
is ThemeFontFamily.DeviceHeadline -> getDeviceHeadlineFontFamily(context)
is ThemeFontFamily.DeviceBody -> getDeviceBodyFontFamily(context)
is ThemeFontFamily.SansSerif -> FontFamily.SansSerif

View File

@@ -1,16 +1,8 @@
package de.mm20.launcher2.ui.theme.typography.fontfamily
import androidx.compose.ui.text.font.FontVariation
import de.mm20.launcher2.ui.R
val GoogleSansFlex = createVariableFontFamily(
fun GoogleSansFlex(settings: Map<String, Float>) = createVariableFontFamily(
R.font.google_sans_flex,
FontVariation.roundness(0f)
settings
)
val GoogleSansFlexRound = createVariableFontFamily(
R.font.google_sans_flex,
FontVariation.roundness( 100f)
)

View File

@@ -1,17 +1,35 @@
package de.mm20.launcher2.ui.theme.typography.fontfamily
import androidx.annotation.FontRes
import androidx.collection.LruCache
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontVariation
import androidx.compose.ui.text.font.FontWeight
import java.io.File
internal fun FontVariation.roundness(roundness: Float): FontVariation.Setting {
return Setting("ROND", roundness)
private val fontCache = LruCache<String, FontFamily>(20)
internal fun createVariableFontFamily(@FontRes resId: Int, settings: Map<String, Float>): FontFamily {
val cacheKey = "0.$resId/${settings.entries.joinToString(separator = ",") { "${it.key}=${it.value}" }}"
val cacheHit = fontCache.get(cacheKey)
if (cacheHit != null) {
return cacheHit
}
val family = createVariableFontFamily(
resId, *settings.map { FontVariation.Setting(it.key, it.value) }.toTypedArray()
)
fontCache.put(cacheKey, family)
return family
}
internal fun createVariableFontFamily(@FontRes resId: Int, vararg settings: FontVariation.Setting): FontFamily {
private fun createVariableFontFamily(@FontRes resId: Int, vararg settings: FontVariation.Setting): FontFamily {
val fonts = mutableListOf<Font>()
for (weight in 100..900 step 100) {
fonts += Font(
@@ -38,4 +56,4 @@ internal fun createVariableFontFamily(@FontRes resId: Int, vararg settings: Font
)
}
return FontFamily(fonts)
}
}

View File

@@ -1,11 +1,6 @@
package de.mm20.launcher2.themes.typography
import android.content.Context
import android.graphics.Typeface
import android.graphics.fonts.SystemFonts
import android.os.Build
import android.util.Log
import androidx.core.graphics.TypefaceCompat
data class FontList(
val builtIn: List<FontFamily>,
@@ -14,6 +9,14 @@ data class FontList(
val system: List<FontFamily>,
)
data class VariableFontAxis(
val name: String,
val label: String,
val range: ClosedFloatingPointRange<Float>,
val step: Float,
val defaultValue: Float,
)
class FontManager(
private val context: Context
) {
@@ -24,12 +27,12 @@ class FontManager(
.getIdentifier("config_bodyFontFamily", "string", "android")
val deviceHeadlineExists = deviceHeadlineResId != 0 &&
context.resources.getString(deviceHeadlineResId).isNotBlank()
context.resources.getString(deviceHeadlineResId).isNotBlank()
val deviceBodyExists = deviceBodyResId != 0 &&
context.resources.getString(deviceBodyResId).isNotBlank()
context.resources.getString(deviceBodyResId).isNotBlank()
return FontList(
builtIn = listOf(FontFamily.LauncherDefault, FontFamily.LauncherDefaultRound),
builtIn = listOf(FontFamily.LauncherDefault()),
generic = listOf(
FontFamily.SansSerif,
FontFamily.Serif,
@@ -42,4 +45,16 @@ class FontManager(
system = emptyList(),
)
}
fun getFontSettings(font: FontFamily.VariableFontFamily): List<VariableFontAxis> {
if (font is FontFamily.LauncherDefault) {
return listOf(
VariableFontAxis("wdth", "Width", 25f..151f, 1f, 100f),
VariableFontAxis("ROND", "Roundness", 0f..100f, 1f, 0f),
VariableFontAxis("GRAD", "Grade", 0f..100f, 1f, 0f),
)
}
return emptyList()
}
}

View File

@@ -181,13 +181,18 @@ data class TextStyle<out W : FontWeight?>(
@Serializable
sealed interface FontFamily {
@Serializable
@SerialName("launcher_default")
data object LauncherDefault : FontFamily
sealed interface VariableFontFamily {
val settings: Map<String, Float>
get() = emptyMap()
}
@Serializable
@SerialName("launcher_default_round")
data object LauncherDefaultRound : FontFamily
@SerialName("launcher_default")
data class LauncherDefault(
override val settings: Map<String, Float> = emptyMap()
) : FontFamily, VariableFontFamily
@Serializable
@SerialName("device_headline")

View File

@@ -78,8 +78,8 @@ class TypographyRepository(
builtIn = true,
name = "Google Sans",
fonts = mapOf(
"brand" to FontFamily.LauncherDefault,
"plain" to FontFamily.LauncherDefault,
"brand" to FontFamily.LauncherDefault(),
"plain" to FontFamily.LauncherDefault(),
),
styles = DefaultTextStyles,
emphasizedStyles = DefaultEmphasizedTextStyles,
@@ -91,8 +91,8 @@ class TypographyRepository(
builtIn = true,
name = "Google Sans (Rounded)",
fonts = mapOf(
"brand" to FontFamily.LauncherDefaultRound,
"plain" to FontFamily.LauncherDefaultRound,
"brand" to FontFamily.LauncherDefault(mapOf("ROND" to 100f)),
"plain" to FontFamily.LauncherDefault(mapOf("ROND" to 100f)),
),
styles = DefaultTextStyles,
emphasizedStyles = DefaultEmphasizedTextStyles,