diff --git a/core/accessibility/build.gradle.kts b/core/accessibility/build.gradle.kts index 6f1670e..9b84f3c 100644 --- a/core/accessibility/build.gradle.kts +++ b/core/accessibility/build.gradle.kts @@ -1,9 +1,18 @@ plugins { id("gwenedeg.kmp.library") + id("gwenedeg.compose") } kotlin { androidLibrary { namespace = "bzh.ajaury.chombev.core.accessibility" } + + sourceSets { + commonMain.dependencies { + // UI - Compose + implementation(libs.compose.runtime) + implementation(libs.compose.ui) + } + } } diff --git a/core/accessibility/src/androidMain/kotlin/bzh/ajaury/chombev/core/accessibility/ui/DynamicTypeFontScale.android.kt b/core/accessibility/src/androidMain/kotlin/bzh/ajaury/chombev/core/accessibility/ui/DynamicTypeFontScale.android.kt new file mode 100644 index 0000000..b9f8c8b --- /dev/null +++ b/core/accessibility/src/androidMain/kotlin/bzh/ajaury/chombev/core/accessibility/ui/DynamicTypeFontScale.android.kt @@ -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() diff --git a/core/accessibility/src/commonMain/kotlin/bzh/ajaury/chombev/core/accessibility/ui/DynamicTypeFontScale.kt b/core/accessibility/src/commonMain/kotlin/bzh/ajaury/chombev/core/accessibility/ui/DynamicTypeFontScale.kt new file mode 100644 index 0000000..e67176a --- /dev/null +++ b/core/accessibility/src/commonMain/kotlin/bzh/ajaury/chombev/core/accessibility/ui/DynamicTypeFontScale.kt @@ -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) diff --git a/core/accessibility/src/iosMain/kotlin/bzh/ajaury/chombev/core/accessibility/ui/DynamicTypeFontScale.ios.kt b/core/accessibility/src/iosMain/kotlin/bzh/ajaury/chombev/core/accessibility/ui/DynamicTypeFontScale.ios.kt new file mode 100644 index 0000000..371a464 --- /dev/null +++ b/core/accessibility/src/iosMain/kotlin/bzh/ajaury/chombev/core/accessibility/ui/DynamicTypeFontScale.ios.kt @@ -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, +) diff --git a/core/accessibility/src/jvmMain/kotlin/bzh/ajaury/chombev/core/accessibility/ui/DynamicTypeFontScale.jvm.kt b/core/accessibility/src/jvmMain/kotlin/bzh/ajaury/chombev/core/accessibility/ui/DynamicTypeFontScale.jvm.kt new file mode 100644 index 0000000..b9f8c8b --- /dev/null +++ b/core/accessibility/src/jvmMain/kotlin/bzh/ajaury/chombev/core/accessibility/ui/DynamicTypeFontScale.jvm.kt @@ -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() diff --git a/core/accessibility/src/webMain/kotlin/bzh/ajaury/chombev/core/accessibility/ui/DynamicTypeFontScale.web.kt b/core/accessibility/src/webMain/kotlin/bzh/ajaury/chombev/core/accessibility/ui/DynamicTypeFontScale.web.kt new file mode 100644 index 0000000..b9f8c8b --- /dev/null +++ b/core/accessibility/src/webMain/kotlin/bzh/ajaury/chombev/core/accessibility/ui/DynamicTypeFontScale.web.kt @@ -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() diff --git a/feature/onboarding/build.gradle.kts b/feature/onboarding/build.gradle.kts index 64e03cf..da88ddc 100644 --- a/feature/onboarding/build.gradle.kts +++ b/feature/onboarding/build.gradle.kts @@ -13,6 +13,9 @@ kotlin { implementation(libs.compose.uiToolingPreview) } commonMain.dependencies { + // Core modules + implementation(projects.core.accessibility) + // Data modules implementation(projects.data.preferences) implementation(projects.data.resources) diff --git a/feature/onboarding/src/commonMain/kotlin/bzh/ajaury/chombev/onboarding/ui/OnboardingScreen.kt b/feature/onboarding/src/commonMain/kotlin/bzh/ajaury/chombev/onboarding/ui/OnboardingScreen.kt index c88af1f..9f8f4bf 100644 --- a/feature/onboarding/src/commonMain/kotlin/bzh/ajaury/chombev/onboarding/ui/OnboardingScreen.kt +++ b/feature/onboarding/src/commonMain/kotlin/bzh/ajaury/chombev/onboarding/ui/OnboardingScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties 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.ui.components.OnboardingInfoPage import bzh.ajaury.chombev.onboarding.ui.components.OnboardingLevelPage @@ -90,19 +91,21 @@ fun OnboardingScreen( // The content sizes itself against the window rather than the platform default width. properties = DialogProperties(usePlatformDefaultWidth = false), ) { - Surface( - modifier = Modifier - .sizeIn(maxWidth = DIALOG_MAX_WIDTH, maxHeight = DIALOG_MAX_HEIGHT) - .fillMaxSize(fraction = DIALOG_WINDOW_FRACTION), - shape = MaterialTheme.shapes.extraLarge, - color = MaterialTheme.colorScheme.surfaceContainerHigh, - ) { - OnboardingContent( - uiState = uiState, - onLevelSelected = onLevelSelected, - onFinish = onFinish, - onClose = onDismiss, - ) + ProvideDynamicTypeFontScale { + Surface( + modifier = Modifier + .sizeIn(maxWidth = DIALOG_MAX_WIDTH, maxHeight = DIALOG_MAX_HEIGHT) + .fillMaxSize(fraction = DIALOG_WINDOW_FRACTION), + shape = MaterialTheme.shapes.extraLarge, + color = MaterialTheme.colorScheme.surfaceContainerHigh, + ) { + OnboardingContent( + uiState = uiState, + onLevelSelected = onLevelSelected, + onFinish = onFinish, + onClose = onDismiss, + ) + } } } } else { diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlaybackSpeedBottomSheet.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlaybackSpeedBottomSheet.kt index f5fbef2..e6996b2 100644 --- a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlaybackSpeedBottomSheet.kt +++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlaybackSpeedBottomSheet.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview 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.preferences.model.PlaybackPreferences import bzh.ajaury.chombev.preferences.ui.PreferenceToggle @@ -50,14 +51,16 @@ fun PlaybackSpeedBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState, ) { - PlaybackSpeedContent( - speed = speed, - breakSeconds = breakSeconds, - autoPause = autoPause, - onSpeedChange = onSpeedChange, - onBreakChange = onBreakChange, - onAutoPauseChange = onAutoPauseChange, - ) + ProvideDynamicTypeFontScale { + PlaybackSpeedContent( + speed = speed, + breakSeconds = breakSeconds, + autoPause = autoPause, + onSpeedChange = onSpeedChange, + onBreakChange = onBreakChange, + onAutoPauseChange = onAutoPauseChange, + ) + } } } diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/ui/SubtitlePreferencesBottomSheet.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/ui/SubtitlePreferencesBottomSheet.kt index 8ce634f..2619a02 100644 --- a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/ui/SubtitlePreferencesBottomSheet.kt +++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/ui/SubtitlePreferencesBottomSheet.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.semantics.setProgress import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.tooling.preview.Preview 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.resources.generated.resources.Res import bzh.ajaury.chombev.resources.generated.resources.subtitle_marked_letters_description @@ -68,14 +69,16 @@ fun SubtitlePreferencesBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState, ) { - SubtitlePreferencesContent( - subtitlePreferences = subtitlePreferences, - onShowTranscriptionChange = onShowTranscriptionChange, - onShowTranslationChange = onShowTranslationChange, - onShowMarkedLettersChange = onShowPhoneticMarkerChange, - onTextSizeDecrease = onTextSizeDecrease, - onTextSizeIncrease = onTextSizeIncrease, - ) + ProvideDynamicTypeFontScale { + SubtitlePreferencesContent( + subtitlePreferences = subtitlePreferences, + onShowTranscriptionChange = onShowTranscriptionChange, + onShowTranslationChange = onShowTranslationChange, + onShowMarkedLettersChange = onShowPhoneticMarkerChange, + onTextSizeDecrease = onTextSizeDecrease, + onTextSizeIncrease = onTextSizeIncrease, + ) + } } } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 9462145..05e4170 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -35,6 +35,7 @@ kotlin { } commonMain.dependencies { // Core modules + implementation(projects.core.accessibility) implementation(projects.core.coroutines) implementation(projects.core.logging) implementation(projects.core.model) diff --git a/shared/src/iosMain/kotlin/bzh/ajaury/chombev/MainViewController.kt b/shared/src/iosMain/kotlin/bzh/ajaury/chombev/MainViewController.kt index f98dad4..bb6765d 100644 --- a/shared/src/iosMain/kotlin/bzh/ajaury/chombev/MainViewController.kt +++ b/shared/src/iosMain/kotlin/bzh/ajaury/chombev/MainViewController.kt @@ -1,6 +1,12 @@ package bzh.ajaury.chombev import androidx.compose.ui.window.ComposeUIViewController +import bzh.ajaury.chombev.core.accessibility.ui.ProvideDynamicTypeFontScale @Suppress("ktlint:standard:function-naming") -fun MainViewController() = ComposeUIViewController { App() } +fun MainViewController() = + ComposeUIViewController { + ProvideDynamicTypeFontScale { + App() + } + }