refactor: add dynamic font scaling support and integrate ProvideDynamicTypeFontScale across modules

This commit is contained in:
Antoine Jaury committed 2026-10-22 14:27:06 +02:00
1 parent df38936893
commit abeb484258
12 files changed
+143 -1

No files matched your search

+9
View File
@@ -1,9 +1,18 @@
plugins { plugins {
id("gwenedeg.kmp.library") id("gwenedeg.kmp.library")
id("gwenedeg.compose")
} }
kotlin { kotlin {
androidLibrary { androidLibrary {
namespace = "bzh.ajaury.chombev.core.accessibility" namespace = "bzh.ajaury.chombev.core.accessibility"
} }
sourceSets {
commonMain.dependencies {
// UI - Compose
implementation(libs.compose.runtime)
implementation(libs.compose.ui)
}
}
} }
@@ -0,0 +1,7 @@
package bzh.ajaury.chombev.core.accessibility.ui
import androidx.compose.runtime.Composable
/** No-op: the font scale already follows the system setting. */
@Composable
actual fun ProvideDynamicTypeFontScale(content: @Composable () -> Unit) = content()
@@ -0,0 +1,13 @@
package bzh.ajaury.chombev.core.accessibility.ui
import androidx.compose.runtime.Composable
/**
* Applies the system text size to [content] as soon as the user changes it.
*
* Only needed on iOS, see its implementation. Every Compose scene must be wrapped: the root one
* and each dialog or bottom sheet, which iOS renders in a scene of its own that does not inherit
* the font scale provided by the root one.
*/
@Composable
expect fun ProvideDynamicTypeFontScale(content: @Composable () -> Unit)
@@ -0,0 +1,80 @@
package bzh.ajaury.chombev.core.accessibility.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import platform.Foundation.NSNotificationCenter
import platform.Foundation.NSOperationQueue
import platform.UIKit.UIApplication
import platform.UIKit.UIContentSizeCategoryAccessibilityExtraExtraExtraLarge
import platform.UIKit.UIContentSizeCategoryAccessibilityExtraExtraLarge
import platform.UIKit.UIContentSizeCategoryAccessibilityExtraLarge
import platform.UIKit.UIContentSizeCategoryAccessibilityLarge
import platform.UIKit.UIContentSizeCategoryAccessibilityMedium
import platform.UIKit.UIContentSizeCategoryDidChangeNotification
import platform.UIKit.UIContentSizeCategoryExtraExtraExtraLarge
import platform.UIKit.UIContentSizeCategoryExtraExtraLarge
import platform.UIKit.UIContentSizeCategoryExtraLarge
import platform.UIKit.UIContentSizeCategoryExtraSmall
import platform.UIKit.UIContentSizeCategoryLarge
import platform.UIKit.UIContentSizeCategoryMedium
import platform.UIKit.UIContentSizeCategorySmall
/**
* Applies the system text size (Dynamic Type) to [content] as soon as the user changes it.
*
* Compose Multiplatform reads the font scale once, when the Compose scene is created, so a change
* made in the iOS settings only shows once the scene is created again: after the app restarts for
* the root scene, or when it is opened again for a dialog or bottom sheet. Workaround for CMP-10365
* (https://youtrack.jetbrains.com/issue/CMP-10365), fixed in Compose Multiplatform 1.13.0: remove
* it once the project is upgraded.
*/
@Composable
actual fun ProvideDynamicTypeFontScale(content: @Composable () -> Unit) {
var contentSizeCategory by remember {
mutableStateOf(UIApplication.sharedApplication.preferredContentSizeCategory)
}
DisposableEffect(Unit) {
val notificationCenter = NSNotificationCenter.defaultCenter
val observer = notificationCenter.addObserverForName(
name = UIContentSizeCategoryDidChangeNotification,
`object` = null,
queue = NSOperationQueue.mainQueue,
) { _ ->
contentSizeCategory = UIApplication.sharedApplication.preferredContentSizeCategory
}
onDispose { notificationCenter.removeObserver(observer) }
}
val density = LocalDensity.current
// Only the font scale is overridden: the pixel density stays the one Compose computed.
val fontScale = contentSizeCategoryToFontScale[contentSizeCategory] ?: density.fontScale
CompositionLocalProvider(
LocalDensity provides Density(density = density.density, fontScale = fontScale),
content = content,
)
}
// Same mapping as Compose Multiplatform (`uiContentSizeCategoryToFontScaleMap`, which is private),
// so that the font scale matches the one Compose computes when the scene is created.
private val contentSizeCategoryToFontScale = mapOf(
UIContentSizeCategoryExtraSmall to 0.8f,
UIContentSizeCategorySmall to 0.85f,
UIContentSizeCategoryMedium to 0.9f,
UIContentSizeCategoryLarge to 1f,
UIContentSizeCategoryExtraLarge to 1.1f,
UIContentSizeCategoryExtraExtraLarge to 1.2f,
UIContentSizeCategoryExtraExtraExtraLarge to 1.3f,
UIContentSizeCategoryAccessibilityMedium to 1.4f,
UIContentSizeCategoryAccessibilityLarge to 1.5f,
UIContentSizeCategoryAccessibilityExtraLarge to 1.6f,
UIContentSizeCategoryAccessibilityExtraExtraLarge to 1.7f,
UIContentSizeCategoryAccessibilityExtraExtraExtraLarge to 1.8f,
)
@@ -0,0 +1,7 @@
package bzh.ajaury.chombev.core.accessibility.ui
import androidx.compose.runtime.Composable
/** No-op: the font scale already follows the system setting. */
@Composable
actual fun ProvideDynamicTypeFontScale(content: @Composable () -> Unit) = content()
@@ -0,0 +1,7 @@
package bzh.ajaury.chombev.core.accessibility.ui
import androidx.compose.runtime.Composable
/** No-op: the font scale already follows the system setting. */
@Composable
actual fun ProvideDynamicTypeFontScale(content: @Composable () -> Unit) = content()
+3
View File
@@ -13,6 +13,9 @@ kotlin {
implementation(libs.compose.uiToolingPreview) implementation(libs.compose.uiToolingPreview)
} }
commonMain.dependencies { commonMain.dependencies {
// Core modules
implementation(projects.core.accessibility)
// Data modules // Data modules
implementation(projects.data.preferences) implementation(projects.data.preferences)
implementation(projects.data.resources) implementation(projects.data.resources)
@@ -33,6 +33,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import bzh.ajaury.chombev.core.accessibility.ui.ProvideDynamicTypeFontScale
import bzh.ajaury.chombev.onboarding.model.OnboardingLevel import bzh.ajaury.chombev.onboarding.model.OnboardingLevel
import bzh.ajaury.chombev.onboarding.ui.components.OnboardingInfoPage import bzh.ajaury.chombev.onboarding.ui.components.OnboardingInfoPage
import bzh.ajaury.chombev.onboarding.ui.components.OnboardingLevelPage import bzh.ajaury.chombev.onboarding.ui.components.OnboardingLevelPage
@@ -90,6 +91,7 @@ fun OnboardingScreen(
// The content sizes itself against the window rather than the platform default width. // The content sizes itself against the window rather than the platform default width.
properties = DialogProperties(usePlatformDefaultWidth = false), properties = DialogProperties(usePlatformDefaultWidth = false),
) { ) {
ProvideDynamicTypeFontScale {
Surface( Surface(
modifier = Modifier modifier = Modifier
.sizeIn(maxWidth = DIALOG_MAX_WIDTH, maxHeight = DIALOG_MAX_HEIGHT) .sizeIn(maxWidth = DIALOG_MAX_WIDTH, maxHeight = DIALOG_MAX_HEIGHT)
@@ -105,6 +107,7 @@ fun OnboardingScreen(
) )
} }
} }
}
} else { } else {
Scaffold { innerPadding -> Scaffold { innerPadding ->
OnboardingContent( OnboardingContent(
@@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import bzh.ajaury.chombev.core.accessibility.ui.ProvideDynamicTypeFontScale
import bzh.ajaury.chombev.player.ui.components.LabeledSlider import bzh.ajaury.chombev.player.ui.components.LabeledSlider
import bzh.ajaury.chombev.preferences.model.PlaybackPreferences import bzh.ajaury.chombev.preferences.model.PlaybackPreferences
import bzh.ajaury.chombev.preferences.ui.PreferenceToggle import bzh.ajaury.chombev.preferences.ui.PreferenceToggle
@@ -50,6 +51,7 @@ fun PlaybackSpeedBottomSheet(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
sheetState = sheetState, sheetState = sheetState,
) { ) {
ProvideDynamicTypeFontScale {
PlaybackSpeedContent( PlaybackSpeedContent(
speed = speed, speed = speed,
breakSeconds = breakSeconds, breakSeconds = breakSeconds,
@@ -59,6 +61,7 @@ fun PlaybackSpeedBottomSheet(
onAutoPauseChange = onAutoPauseChange, onAutoPauseChange = onAutoPauseChange,
) )
} }
}
} }
@Composable @Composable
@@ -32,6 +32,7 @@ import androidx.compose.ui.semantics.setProgress
import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import bzh.ajaury.chombev.core.accessibility.ui.ProvideDynamicTypeFontScale
import bzh.ajaury.chombev.preferences.model.SubtitlePreferences import bzh.ajaury.chombev.preferences.model.SubtitlePreferences
import bzh.ajaury.chombev.resources.generated.resources.Res import bzh.ajaury.chombev.resources.generated.resources.Res
import bzh.ajaury.chombev.resources.generated.resources.subtitle_marked_letters_description import bzh.ajaury.chombev.resources.generated.resources.subtitle_marked_letters_description
@@ -68,6 +69,7 @@ fun SubtitlePreferencesBottomSheet(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
sheetState = sheetState, sheetState = sheetState,
) { ) {
ProvideDynamicTypeFontScale {
SubtitlePreferencesContent( SubtitlePreferencesContent(
subtitlePreferences = subtitlePreferences, subtitlePreferences = subtitlePreferences,
onShowTranscriptionChange = onShowTranscriptionChange, onShowTranscriptionChange = onShowTranscriptionChange,
@@ -77,6 +79,7 @@ fun SubtitlePreferencesBottomSheet(
onTextSizeIncrease = onTextSizeIncrease, onTextSizeIncrease = onTextSizeIncrease,
) )
} }
}
} }
@Composable @Composable
+1
View File
@@ -35,6 +35,7 @@ kotlin {
} }
commonMain.dependencies { commonMain.dependencies {
// Core modules // Core modules
implementation(projects.core.accessibility)
implementation(projects.core.coroutines) implementation(projects.core.coroutines)
implementation(projects.core.logging) implementation(projects.core.logging)
implementation(projects.core.model) implementation(projects.core.model)
@@ -1,6 +1,12 @@
package bzh.ajaury.chombev package bzh.ajaury.chombev
import androidx.compose.ui.window.ComposeUIViewController import androidx.compose.ui.window.ComposeUIViewController
import bzh.ajaury.chombev.core.accessibility.ui.ProvideDynamicTypeFontScale
@Suppress("ktlint:standard:function-naming") @Suppress("ktlint:standard:function-naming")
fun MainViewController() = ComposeUIViewController { App() } fun MainViewController() =
ComposeUIViewController {
ProvideDynamicTypeFontScale {
App()
}
}