feat: experimental font selection with google sans flex

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2025-11-29 13:33:56 +01:00
parent e07660c0e2
commit a6883f2b8e
14 changed files with 378 additions and 4 deletions

View File

@ -146,6 +146,7 @@ fun InviteScreen(
StoatTheme(
requestedTheme = LoadedSettings.theme,
requestedUserInterfaceFont = LoadedSettings.font,
colourOverrides = SyncedSettings.android.colourOverrides
) {
Surface(

View File

@ -481,6 +481,7 @@ fun AppEntrypoint(
StoatTheme(
requestedTheme = LoadedSettings.theme,
requestedUserInterfaceFont = LoadedSettings.font,
colourOverrides = SyncedSettings.android.colourOverrides
) {
Box(

View File

@ -279,6 +279,7 @@ fun ShareTargetScreen(
StoatTheme(
requestedTheme = LoadedSettings.theme,
requestedUserInterfaceFont = LoadedSettings.font,
colourOverrides = SyncedSettings.android.colourOverrides
) {
Scaffold(

View File

@ -184,6 +184,7 @@ fun ImageViewScreen(resource: AutumnResource, onClose: () -> Unit = {}) {
StoatTheme(
requestedTheme = LoadedSettings.theme,
requestedUserInterfaceFont = LoadedSettings.font,
colourOverrides = SyncedSettings.android.colourOverrides
) {
Scaffold(

View File

@ -58,6 +58,7 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.stoat.R
import chat.stoat.api.settings.UserInterfaceFont
import chat.stoat.composables.generic.Presence
import chat.stoat.composables.generic.RemoteImage
import chat.stoat.composables.generic.presenceColour
@ -81,7 +82,8 @@ class IncomingActivity : ComponentActivity() {
@Composable
fun IncomingCall() {
StoatTheme(
requestedTheme = if (isSystemInDarkTheme()) Theme.Default else Theme.Light
requestedTheme = if (isSystemInDarkTheme()) Theme.Default else Theme.Light,
requestedUserInterfaceFont = UserInterfaceFont.Default
) {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) {
IncomingCallInner()

View File

@ -43,6 +43,11 @@ data class AndroidSpecificSettings(
* Can be one of `{ None, Default, Light, M3Dynamic, Amoled }`
*/
var theme: String? = null,
/**
* The font to use for the app.
* Can be one of `{ Default, GoogleSansFlex }`
*/
var font: String? = null,
/**
* Colour overrides.
* Map of `primary, onPrimary, primaryContainer, onPrimaryContainer, inversePrimary, secondary, onSecondary, secondaryContainer, onSecondaryContainer, tertiary, onTertiary, tertiaryContainer, onTertiaryContainer, background, onBackground, surface, onSurface, surfaceVariant, onSurfaceVariant, surfaceTint, inverseSurface, inverseOnSurface, error, onError, errorContainer, onErrorContainer, outline, outlineVariant, scrim` to int colours.

View File

@ -14,6 +14,11 @@ enum class MessageReplyStyle {
DoubleTap
}
enum class UserInterfaceFont {
Default,
GoogleSansFlex,
}
typealias SpecialEmbedSettings = AndroidSpecificSettingsSpecialEmbedSettings
object LoadedSettings {
@ -23,6 +28,7 @@ object LoadedSettings {
var experimentsEnabled by mutableStateOf(false)
var specialEmbedSettings by mutableStateOf(SpecialEmbedSettings())
var poorlyFormedSettingsKeys by mutableStateOf(emptySet<String>())
var font by mutableStateOf(UserInterfaceFont.Default)
fun hydrateWithSettings(settings: SyncedSettings) {
this.theme = settings.android.theme?.let {
@ -33,6 +39,13 @@ object LoadedSettings {
?: MessageReplyStyle.SwipeFromEnd
this.avatarRadius = settings.android.avatarRadius ?: 50
this.specialEmbedSettings = settings.android.specialEmbedSettings ?: SpecialEmbedSettings()
this.font = settings.android.font?.let {
try {
UserInterfaceFont.valueOf(it)
} catch (e: Exception) {
null
}
} ?: UserInterfaceFont.Default
}
fun reset() {
@ -41,5 +54,6 @@ object LoadedSettings {
avatarRadius = 50
specialEmbedSettings = SpecialEmbedSettings()
poorlyFormedSettingsKeys = emptySet()
font = UserInterfaceFont.Default
}
}

View File

@ -27,6 +27,7 @@ object SyncedSettings {
private val _android = mutableStateOf(
AndroidSpecificSettings(
theme = "None",
font = "Default",
colourOverrides = null,
messageReplyStyle = "None"
)
@ -130,6 +131,7 @@ object SyncedSettings {
suspend fun resetAndroid() {
val default = AndroidSpecificSettings(
theme = "None",
font = "Default",
colourOverrides = null,
messageReplyStyle = "None"
)

View File

@ -23,7 +23,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ButtonGroupDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar
@ -32,6 +34,7 @@ import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.ToggleButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.rememberModalBottomSheetState
@ -50,6 +53,9 @@ import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
@ -62,10 +68,13 @@ import chat.stoat.api.StoatCbor
import chat.stoat.api.StoatJson
import chat.stoat.api.settings.LoadedSettings
import chat.stoat.api.settings.SyncedSettings
import chat.stoat.api.settings.UserInterfaceFont
import chat.stoat.composables.generic.ListHeader
import chat.stoat.composables.screens.settings.appearance.ColourChip
import chat.stoat.composables.screens.settings.appearance.CornerRadiusPicker
import chat.stoat.sheets.ColourPickerSheet
import chat.stoat.ui.theme.GoogleSansFlex
import chat.stoat.ui.theme.Inter
import chat.stoat.ui.theme.OverridableColourScheme
import chat.stoat.ui.theme.Theme
import chat.stoat.ui.theme.getFieldByName
@ -96,6 +105,13 @@ class AppearanceSettingsScreenViewModel @Inject constructor(
}
}
fun saveNewFont(font: UserInterfaceFont) {
LoadedSettings.font = font
viewModelScope.launch {
SyncedSettings.updateAndroid(SyncedSettings.android.copy(font = font.name))
}
}
fun saveNewAvatarRadius(radius: Int) {
LoadedSettings.avatarRadius = radius
viewModelScope.launch {
@ -198,7 +214,10 @@ class AppearanceSettingsScreenViewModel @Inject constructor(
}
}
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
@OptIn(
ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class,
ExperimentalMaterial3ExpressiveApi::class
)
@Composable
fun AppearanceSettingsScreen(
navController: NavController,
@ -391,6 +410,45 @@ fun AppearanceSettingsScreen(
}
}
ListHeader {
Text(stringResource(R.string.settings_appearance_typeface))
}
FlowRow(
Modifier
.padding(horizontal = 8.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween),
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
ToggleButton(
checked = LoadedSettings.font == UserInterfaceFont.Default,
onCheckedChange = { viewModel.saveNewFont(UserInterfaceFont.Default) },
shapes = ButtonGroupDefaults.connectedLeadingButtonShapes(),
modifier = Modifier
.semantics { role = Role.RadioButton }
.weight(1f),
) {
Text(
stringResource(R.string.settings_appearance_typeface_default),
fontFamily = Inter
)
}
ToggleButton(
checked = LoadedSettings.font == UserInterfaceFont.GoogleSansFlex,
onCheckedChange = { viewModel.saveNewFont(UserInterfaceFont.GoogleSansFlex) },
shapes = ButtonGroupDefaults.connectedTrailingButtonShapes(),
modifier = Modifier
.semantics { role = Role.RadioButton }
.weight(1f),
) {
Text(
stringResource(R.string.settings_appearance_typeface_google_sans_flex),
fontFamily = GoogleSansFlex
)
}
}
ListHeader {
Text(stringResource(R.string.settings_appearance_avatar_shape))
}

View File

@ -17,6 +17,7 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.ViewCompat
import chat.stoat.api.settings.UserInterfaceFont
val LightColorScheme = lightColorScheme(
primary = Colour.PrimaryLight,
@ -172,14 +173,19 @@ fun getColorScheme(
@Composable
fun StoatTheme(
requestedTheme: Theme,
requestedUserInterfaceFont: UserInterfaceFont,
colourOverrides: OverridableColourScheme? = null,
content: @Composable () -> Unit
) {
val colorScheme = getColorScheme(requestedTheme, colourOverrides)
val typography = when (requestedUserInterfaceFont) {
UserInterfaceFont.Default -> StoatTypography
UserInterfaceFont.GoogleSansFlex -> GoogleTypography
}
MaterialTheme(
colorScheme = colorScheme,
typography = StoatTypography,
typography = typography,
content = content
)
}

View File

@ -1,15 +1,17 @@
package chat.stoat.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextStyle
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 androidx.compose.ui.unit.sp
import chat.stoat.R
private val Inter = FontFamily(
val Inter = FontFamily(
Font(R.font.inter_thin, FontWeight.Thin),
Font(R.font.inter_extralight, FontWeight.ExtraLight),
Font(R.font.inter_light, FontWeight.Light),
@ -54,6 +56,190 @@ val FragmentMono = FontFamily(
Font(R.font.fragmentmono_italic, FontWeight.Normal, FontStyle.Italic)
)
@OptIn(ExperimentalTextApi::class)
val GoogleSansFlex = FontFamily(
Font(
R.font.googlesansflex,
FontWeight.Thin,
FontStyle.Normal,
variationSettings = FontVariation.Settings(
FontVariation.weight(100),
FontVariation.width(100f),
FontVariation.slant(0f),
)
),
Font(
R.font.googlesansflex,
FontWeight.ExtraLight,
FontStyle.Normal,
variationSettings = FontVariation.Settings(
FontVariation.weight(200),
FontVariation.width(100f),
FontVariation.slant(0f),
)
),
Font(
R.font.googlesansflex,
FontWeight.Light,
FontStyle.Normal,
variationSettings = FontVariation.Settings(
FontVariation.weight(300),
FontVariation.width(100f),
FontVariation.slant(0f),
)
),
Font(
R.font.googlesansflex,
FontWeight.Normal,
FontStyle.Normal,
variationSettings = FontVariation.Settings(
FontVariation.weight(400),
FontVariation.width(100f),
FontVariation.slant(0f),
)
),
Font(
R.font.googlesansflex,
FontWeight.Medium,
FontStyle.Normal,
variationSettings = FontVariation.Settings(
FontVariation.weight(500),
FontVariation.width(100f),
FontVariation.slant(0f),
)
),
Font(
R.font.googlesansflex,
FontWeight.SemiBold,
FontStyle.Normal,
variationSettings = FontVariation.Settings(
FontVariation.weight(600),
FontVariation.width(100f),
FontVariation.slant(0f),
)
),
Font(
R.font.googlesansflex,
FontWeight.Bold,
FontStyle.Normal,
variationSettings = FontVariation.Settings(
FontVariation.weight(700),
FontVariation.width(100f),
FontVariation.slant(0f),
)
),
Font(
R.font.googlesansflex,
FontWeight.ExtraBold,
FontStyle.Normal,
variationSettings = FontVariation.Settings(
FontVariation.weight(800),
FontVariation.width(100f),
FontVariation.slant(0f),
)
),
Font(
R.font.googlesansflex,
FontWeight.Black,
FontStyle.Normal,
variationSettings = FontVariation.Settings(
FontVariation.weight(900),
FontVariation.width(100f),
FontVariation.slant(0f),
)
),
Font(
R.font.googlesansflex,
FontWeight.Thin,
FontStyle.Italic,
variationSettings = FontVariation.Settings(
FontVariation.weight(100),
FontVariation.width(100f),
FontVariation.slant(-10f),
)
),
Font(
R.font.googlesansflex,
FontWeight.ExtraLight,
FontStyle.Italic,
variationSettings = FontVariation.Settings(
FontVariation.weight(200),
FontVariation.width(100f),
FontVariation.slant(-10f),
)
),
Font(
R.font.googlesansflex,
FontWeight.Light,
FontStyle.Italic,
variationSettings = FontVariation.Settings(
FontVariation.weight(300),
FontVariation.width(100f),
FontVariation.slant(-10f),
)
),
Font(
R.font.googlesansflex,
FontWeight.Normal,
FontStyle.Italic,
variationSettings = FontVariation.Settings(
FontVariation.weight(400),
FontVariation.width(100f),
FontVariation.slant(-10f),
)
),
Font(
R.font.googlesansflex,
FontWeight.Medium,
FontStyle.Italic,
variationSettings = FontVariation.Settings(
FontVariation.weight(500),
FontVariation.width(100f),
FontVariation.slant(-10f),
)
),
Font(
R.font.googlesansflex,
FontWeight.SemiBold,
FontStyle.Italic,
variationSettings = FontVariation.Settings(
FontVariation.weight(600),
FontVariation.width(100f),
FontVariation.slant(-10f),
)
),
Font(
R.font.googlesansflex,
FontWeight.Bold,
FontStyle.Italic,
variationSettings = FontVariation.Settings(
FontVariation.weight(700),
FontVariation.width(100f),
FontVariation.slant(-10f),
)
),
Font(
R.font.googlesansflex,
FontWeight.ExtraBold,
FontStyle.Italic,
variationSettings = FontVariation.Settings(
FontVariation.weight(800),
FontVariation.width(100f),
FontVariation.slant(-10f),
)
),
Font(
R.font.googlesansflex,
FontWeight.Black,
FontStyle.Italic,
variationSettings = FontVariation.Settings(
FontVariation.weight(900),
FontVariation.width(100f),
FontVariation.slant(-10f),
)
)
)
val StoatTypography = Typography(
displayLarge = TextStyle(
fontFamily = InterDisplay,
@ -135,3 +321,81 @@ val StoatTypography = Typography(
fontSize = 12.sp
)
)
val GoogleTypography = Typography(
displayLarge = TextStyle(
fontFamily = GoogleSansFlex,
fontWeight = FontWeight.Black,
fontSize = 57.sp
),
displayMedium = TextStyle(
fontFamily = GoogleSansFlex,
fontWeight = FontWeight.ExtraBold,
fontSize = 45.sp
),
displaySmall = TextStyle(
fontFamily = GoogleSansFlex,
fontWeight = FontWeight.Bold,
fontSize = 36.sp
),
headlineLarge = TextStyle(
fontFamily = GoogleSansFlex,
fontWeight = FontWeight.SemiBold,
fontSize = 32.sp
),
headlineMedium = TextStyle(
fontFamily = GoogleSansFlex,
fontWeight = FontWeight.SemiBold,
fontSize = 28.sp
),
headlineSmall = TextStyle(
fontFamily = GoogleSansFlex,
fontWeight = FontWeight.Bold,
fontSize = 24.sp
),
titleLarge = TextStyle(
fontFamily = GoogleSansFlex,
fontWeight = FontWeight.SemiBold,
fontSize = 22.sp
),
titleMedium = TextStyle(
fontFamily = GoogleSansFlex,
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp
),
titleSmall = TextStyle(
fontFamily = GoogleSansFlex,
fontWeight = FontWeight.Bold,
fontSize = 14.sp
),
labelLarge = TextStyle(
fontFamily = GoogleSansFlex,
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp
),
labelMedium = TextStyle(
fontFamily = GoogleSansFlex,
fontWeight = FontWeight.Bold,
fontSize = 12.sp
),
labelSmall = TextStyle(
fontFamily = GoogleSansFlex,
fontWeight = FontWeight.Bold,
fontSize = 11.sp
),
bodyLarge = TextStyle(
fontFamily = GoogleSansFlex,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
),
bodyMedium = TextStyle(
fontFamily = GoogleSansFlex,
fontWeight = FontWeight.Normal,
fontSize = 14.sp
),
bodySmall = TextStyle(
fontFamily = GoogleSansFlex,
fontWeight = FontWeight.Bold,
fontSize = 12.sp
)
)

Binary file not shown.

View File

@ -708,6 +708,9 @@
<string name="settings_appearance_theme_m3dynamic">Material You</string>
<string name="settings_appearance_theme_m3dynamic_unsupported">Material You (unsupported)</string>
<string name="settings_appearance_theme_m3dynamic_unsupported_toast">Material You is not supported on this device.</string>
<string name="settings_appearance_typeface">Font</string>
<string name="settings_appearance_typeface_default">Default</string>
<string name="settings_appearance_typeface_google_sans_flex" translatable="false">Google Sans Flex</string>
<string name="settings_appearance_avatar_shape">Profile Picture Shape</string>
<string name="settings_appearance_avatar_shape_description">Choose the rounding grade for profile pictures, including in chat and profiles. This applies to all users.</string>

View File

@ -0,0 +1,16 @@
{
"uniqueId": "com.google.fonts:googlesansflex:0.0.0",
"developers": [
{
"name": "Google",
"organisationUrl": "https://about.google/"
}
],
"artifactVersion": "0.0.0",
"description": "Google Sans Flex is the next generation of Googles brand typeface.",
"name": "Google Sans Flex",
"website": "https://fonts.google.com/specimen/Google+Sans+Flex",
"licenses": [
"OFL"
]
}