diff --git a/compose-ktx/CHANGELOG.md b/compose-ktx/CHANGELOG.md
new file mode 100644
index 0000000..ec88929
--- /dev/null
+++ b/compose-ktx/CHANGELOG.md
@@ -0,0 +1,10 @@
+## Unreleased
+
+### Changed
+
+- Create compose-ktx
+ added:
+ - `Modifier.applyIf` - Applies the given block of modifications to the Modifier if the condition is true
+ - `Modifier.applyChoice` - Chooses between two blocks of modifications based on a condition and returns the resulting Modifier
+ - `SystemBarsStyleEffect` - changes the system UI style on lifecycle event ON_START, at ON_STOP returns the previous style
+ - `FixedFontScaleContainer` - A container that fixes the font scale, ignoring values, that are set in the phone's system settings
diff --git a/compose-ktx/README.md b/compose-ktx/README.md
new file mode 100644
index 0000000..9ced6ec
--- /dev/null
+++ b/compose-ktx/README.md
@@ -0,0 +1,47 @@
+# compose-ktx
+[![Version](https://img.shields.io/maven-central/v/com.redmadrobot.extensions/compose-ktx?style=flat-square)][mavenCentral]
+[![License](https://img.shields.io/github/license/RedMadRobot/redmadrobot-android-ktx?style=flat-square)][license]
+
+---
+
+
+
+- [Installation](#installation)
+- [Usage](#usage)
+- [Contributing](#contributing)
+
+
+
+Extended set of extensions for compose.
+
+## Installation
+
+Add the dependency:
+```groovy
+repositories {
+ mavenCentral()
+ google()
+}
+
+dependencies {
+ implementation("com.redmadrobot.extensions:compose-ktx:")
+}
+```
+
+## Usage
+
+| Extension | Description |
+|:--------------------|:-----------|
+| `Modifier.applyIf` | Applies the given block of modifications to the Modifier if the condition is true |
+| `Modifier.applyChoice` | Chooses between two blocks of modifications based on a condition and returns the resulting Modifier |
+| `SystemBarsStyleEffect` | changes the system UI style on lifecycle event ON_START, at ON_STOP returns the previous style |
+| `FixedFontScaleContainer` | A container that fixes the font scale, ignoring values, that are set in the phone's system settings |
+
+## Contributing
+
+Merge requests are welcome.
+For major changes, please open an issue first to discuss what you would like to change.
+
+
+[mavenCentral]: https://search.maven.org/artifact/com.redmadrobot.extensions/compose-ktx
+[license]: ../LICENSE
diff --git a/compose-ktx/build.gradle.kts b/compose-ktx/build.gradle.kts
new file mode 100644
index 0000000..2a614dc
--- /dev/null
+++ b/compose-ktx/build.gradle.kts
@@ -0,0 +1,32 @@
+plugins {
+ id("com.redmadrobot.android-library")
+ id("com.redmadrobot.publish")
+ convention.library.android
+}
+
+version = "1.6.6-0"
+description = "Compose extensions"
+
+redmadrobot {
+ android.minSdk = 21
+}
+
+android {
+ namespace = "com.redmadrobot.compose-ktx"
+
+ buildFeatures {
+ compose = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = androidx.versions.compose.compiler.get()
+ }
+}
+
+dependencies {
+ api(androidx.compose.ui)
+ api(androidx.annotation)
+ api(androidx.compose.runtime)
+ api(androidx.core)
+ api(androidx.lifecycle.common)
+}
diff --git a/compose-ktx/src/main/kotlin/FixedFontScaleContainer.kt b/compose-ktx/src/main/kotlin/FixedFontScaleContainer.kt
new file mode 100644
index 0000000..f07d571
--- /dev/null
+++ b/compose-ktx/src/main/kotlin/FixedFontScaleContainer.kt
@@ -0,0 +1,44 @@
+@file:Suppress("FunctionNaming")
+package com.redmadrobot.extensions.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Density
+
+/**
+ * A container that fixes the font scale, ignoring values,
+ * that are set in the phone's system settings
+ */
+@Composable
+public fun FixedFontScaleContainer(
+ content: @Composable () -> Unit,
+) {
+ val fixedFontScaleDensity = Density(LocalDensity.current.density)
+ CompositionLocalProvider(
+ LocalDensity provides fixedFontScaleDensity,
+ content = content,
+ )
+}
+
+/**
+ * A container that restricts the font scale, ignoring values,
+ * that are set in the phone's system settings
+ *
+ * @param limit - the upper limit of font enlargement
+ */
+@Composable
+public fun LimitedFontScaleContainer(
+ limit: Float,
+ content: @Composable () -> Unit,
+) {
+ val fontScale = LocalDensity.current.fontScale.coerceAtMost(limit)
+ val fixedFontScaleDensity = Density(
+ density = LocalDensity.current.density,
+ fontScale = fontScale,
+ )
+ CompositionLocalProvider(
+ LocalDensity provides fixedFontScaleDensity,
+ content = content,
+ )
+}
diff --git a/compose-ktx/src/main/kotlin/modifier/ApplyIf.kt b/compose-ktx/src/main/kotlin/modifier/ApplyIf.kt
new file mode 100644
index 0000000..8206cf9
--- /dev/null
+++ b/compose-ktx/src/main/kotlin/modifier/ApplyIf.kt
@@ -0,0 +1,32 @@
+package com.redmadrobot.extensions.compose.modifier
+
+import androidx.compose.ui.Modifier
+
+/**
+ * Applies the given block of modifications to the Modifier if the condition is true.
+ *
+ * @param condition condition to determine if the block should be applied.
+ * @param block Lambda function that modifies the Modifier.
+ * @return Modified Modifier based on the condition.
+ */
+public inline fun Modifier.applyIf(
+ condition: Boolean,
+ block: Modifier.() -> Modifier,
+): Modifier = applyChoice(condition = condition, trueBlock = block, falseBlock = { this })
+
+/**
+ * Chooses between two blocks of modifications based on a condition and returns the resulting Modifier.
+ *
+ * @param condition Boolean condition to determine which block to apply.
+ * @param trueBlock Lambda function to modify the Modifier if the condition is true.
+ * @param falseBlock Lambda function to modify the Modifier if the condition is false.
+ * @return Modified Modifier based on the condition.
+ */
+public inline fun Modifier.applyChoice(
+ condition: Boolean,
+ trueBlock: Modifier.() -> Modifier,
+ falseBlock: Modifier.() -> Modifier,
+): Modifier {
+ val modifier = if (condition) trueBlock() else falseBlock()
+ return this.then(modifier)
+}
diff --git a/compose-ktx/src/main/kotlin/systemui/NavigationBarStyleEffect.kt b/compose-ktx/src/main/kotlin/systemui/NavigationBarStyleEffect.kt
new file mode 100644
index 0000000..97b3dcf
--- /dev/null
+++ b/compose-ktx/src/main/kotlin/systemui/NavigationBarStyleEffect.kt
@@ -0,0 +1,50 @@
+@file:Suppress("FunctionNaming")
+package com.redmadrobot.extensions.compose.systemui
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+
+/**
+ * Changes the color of navigation icons on [Event.ON_START], and reverts to the previous color on [Event.ON_STOP].
+ * @param darkIcons Whether to use dark or light icons in navigation
+ * @param withScrim Whether to add a scrim to the navigation for additional contrast of icons
+ */
+@Composable
+public fun NavigationBarStyleEffect(
+ darkIcons: Boolean,
+ withScrim: Boolean = false,
+) {
+ NavigationBarStyleEffect(
+ color = scrimOrTransparent(withScrim, darkIcons),
+ darkIcons = darkIcons,
+ )
+}
+
+/**
+ * Changes the color of the navigation on [Event.ON_START], and reverts to the previous color on [Event.ON_STOP].
+ * The color of the navigation icons will be automatically selected based on the provided color.
+ * @param color The color to which the navigation will be recolored
+ */
+@Composable
+public fun NavigationBarStyleEffect(color: Color) {
+ NavigationBarStyleEffect(
+ color = color,
+ darkIcons = color.isLight(),
+ )
+}
+
+/**
+ * Changes the style of navigation on [Event.ON_START], and reverts to the previous style on [Event.ON_STOP].
+ * @param color The color to which the navigation will be recolored
+ * @param darkIcons Whether to use dark or light icons in navigation
+ */
+@Composable
+public fun NavigationBarStyleEffect(
+ color: Color,
+ darkIcons: Boolean,
+) {
+ SystemBarsStyleEffect(
+ statusBarStyle = SystemBarStyle.Unspecified,
+ navigationBarStyle = rememberSystemBarStyle(color, darkIcons),
+ )
+}
diff --git a/compose-ktx/src/main/kotlin/systemui/StatusBarStyleEffect.kt b/compose-ktx/src/main/kotlin/systemui/StatusBarStyleEffect.kt
new file mode 100644
index 0000000..ea1d4d4
--- /dev/null
+++ b/compose-ktx/src/main/kotlin/systemui/StatusBarStyleEffect.kt
@@ -0,0 +1,38 @@
+@file:Suppress("FunctionNaming")
+package com.redmadrobot.extensions.compose.systemui
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.lifecycle.Lifecycle.Event
+
+/**
+ * When [Event.ON_START] changes the color of status bar icons, when [Event.ON_STOP] returns the previous color
+ * @param darkIcons Whether to use dark or light icons in the status bar
+ * @param withScrim Whether to add darkening/lightening of the status bar for additional contrast of icons
+ */
+@Composable
+public fun StatusBarStyleEffect(
+ darkIcons: Boolean,
+ withScrim: Boolean = false,
+) {
+ StatusBarStyleEffect(
+ color = scrimOrTransparent(withScrim, darkIcons),
+ darkIcons = darkIcons,
+ )
+}
+
+/**
+ * When [Event.ON_START] changes the status bar style, when [Event.ON_STOP] returns the previous style
+ * @param color The color in which the status bar will be repainted
+ * @param darkIcons Whether to use dark or light icons in the status bar
+ */
+@Composable
+public fun StatusBarStyleEffect(
+ color: Color,
+ darkIcons: Boolean,
+) {
+ SystemBarsStyleEffect(
+ statusBarStyle = rememberSystemBarStyle(color, darkIcons),
+ navigationBarStyle = SystemBarStyle.Unspecified,
+ )
+}
diff --git a/compose-ktx/src/main/kotlin/systemui/SystemBarsStyleEffect.kt b/compose-ktx/src/main/kotlin/systemui/SystemBarsStyleEffect.kt
new file mode 100644
index 0000000..cbcf673
--- /dev/null
+++ b/compose-ktx/src/main/kotlin/systemui/SystemBarsStyleEffect.kt
@@ -0,0 +1,217 @@
+@file:Suppress("FunctionNaming")
+package com.redmadrobot.extensions.compose.systemui
+
+import android.app.Activity
+import android.os.Build
+import android.view.Window
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.graphics.isSpecified
+import androidx.compose.ui.graphics.luminance
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.core.view.WindowCompat
+import androidx.lifecycle.Lifecycle.Event
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleOwner
+
+/**
+ * When [Event.ON_START] changes the system UI style, when [Event.ON_STOP] returns the previous style.
+ * @param darkIcons Whether to use dark or light icons
+ * @param withScrim Whether to add darkening/lightening for additional icon contrast
+ */
+@Composable
+public fun SystemBarsStyleEffect(
+ darkIcons: Boolean,
+ withScrim: Boolean = false,
+) {
+ SystemBarsStyleEffect(
+ color = scrimOrTransparent(withScrim, darkIcons),
+ darkIcons = darkIcons,
+ )
+}
+
+/**
+ * When [Event.ON_START] changes the system UI style, when [Event.ON_STOP] returns the previous style
+ * @param color The color in which the system bars will be repainted
+ * @param darkIcons Whether to use dark or light icons in system bars
+ */
+@Composable
+public fun SystemBarsStyleEffect(
+ color: Color,
+ darkIcons: Boolean,
+) {
+ val style = rememberSystemBarStyle(color = color, darkIcons = darkIcons)
+ SystemBarsStyleEffect(
+ statusBarStyle = style,
+ navigationBarStyle = style
+ )
+}
+
+/**
+ * At [Event.ON_START] changes the system UI style, at [Event.ON_STOP] returns the previous style
+ * @param statusBarStyle Status Bar Style
+ * @param navigationBarStyle Navigation style
+ */
+@Composable
+public fun SystemBarsStyleEffect(
+ statusBarStyle: SystemBarStyle,
+ navigationBarStyle: SystemBarStyle,
+) {
+ val window = (LocalContext.current as? Activity)?.window ?: return
+ val statusBarColorChanger = rememberStatusBarColorChanger(window, statusBarStyle, navigationBarStyle)
+ val lifecycle = LocalLifecycleOwner.current.lifecycle
+
+ DisposableEffect(statusBarColorChanger, lifecycle) {
+ lifecycle.addObserver(statusBarColorChanger)
+ onDispose { lifecycle.removeObserver(statusBarColorChanger) }
+ }
+}
+
+@Composable
+private fun rememberStatusBarColorChanger(
+ window: Window,
+ statusBarStyle: SystemBarStyle,
+ navigationBarStyle: SystemBarStyle,
+): SystemBarsStyleChanger {
+ return remember(window, statusBarStyle, navigationBarStyle) {
+ SystemBarsStyleChanger(window, statusBarStyle, navigationBarStyle)
+ }
+}
+
+private class SystemBarsStyleChanger(
+ private val window: Window,
+ private val statusBarStyle: SystemBarStyle,
+ private val navigationBarStyle: SystemBarStyle,
+) : LifecycleEventObserver {
+
+ private val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
+
+ private var originalStatusStyle = SystemBarStyle.Unspecified
+ private var originalNavigationStyle = SystemBarStyle.Unspecified
+
+ override fun onStateChanged(source: LifecycleOwner, event: Event) {
+ when (event) {
+ Event.ON_START -> {
+ if (statusBarStyle.isSpecified) setStatusBarAppearance()
+ if (navigationBarStyle.isSpecified) setNavigationBarAppearance()
+ }
+
+ Event.ON_STOP -> {
+ if (statusBarStyle.isSpecified) resetStatusBarAppearance()
+ if (navigationBarStyle.isSpecified) resetNavigationBarAppearance()
+ }
+
+ else -> Unit // no-op
+ }
+ }
+
+ // region Status Bar
+ private fun setStatusBarAppearance() {
+ originalStatusStyle = SystemBarStyle(
+ color = Color(window.statusBarColor),
+ darkIcons = windowInsetsController.isAppearanceLightStatusBars,
+ )
+ setStatusBarColor(statusBarStyle)
+ }
+
+ private fun resetStatusBarAppearance() {
+ setStatusBarColor(originalStatusStyle)
+ }
+
+ private fun setStatusBarColor(style: SystemBarStyle) {
+ windowInsetsController.isAppearanceLightStatusBars = style.darkIcons
+ style.useColor { window.statusBarColor = it }
+ }
+ // endregion
+
+ // region Navigation Bar
+ private fun setNavigationBarAppearance() {
+ originalNavigationStyle = SystemBarStyle(
+ color = Color(window.navigationBarColor),
+ darkIcons = windowInsetsController.isAppearanceLightNavigationBars,
+ )
+ setNavigationBarColor(navigationBarStyle)
+ }
+
+ private fun resetNavigationBarAppearance() {
+ setNavigationBarColor(originalNavigationStyle)
+ }
+
+ @Suppress("BooleanPropertyNaming")
+ private fun setNavigationBarColor(style: SystemBarStyle) {
+ windowInsetsController.isAppearanceLightNavigationBars = style.darkIcons
+ // If we're set to use dark icons, but our windowInsetsController call didn't
+ // succeed (usually due to API level), we instead transform the color to maintain
+ // contrast
+ val failedToApplyDarkIcons = style.darkIcons && !windowInsetsController.isAppearanceLightNavigationBars
+ style.useColor(scrimmed = failedToApplyDarkIcons) { color ->
+ window.navigationBarColor = color
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ window.navigationBarDividerColor = color
+ }
+ }
+ }
+ // endregion
+}
+
+/**
+ * Creates a [SystemBarStyle] style with the given parameters.
+ * @param color The color in which the system bar will be repainted
+ * @param darkIcons Whether to use dark or light icons in the system bar
+ * @see SystemBarsStyleEffect
+ */
+@Composable
+public fun rememberSystemBarStyle(color: Color, darkIcons: Boolean): SystemBarStyle {
+ return remember(color, darkIcons) { SystemBarStyle(color, darkIcons) }
+}
+
+/** @see rememberSystemBarStyle */
+@Suppress("BooleanPropertyNaming")
+@Immutable
+public data class SystemBarStyle(
+ val color: Color,
+ val darkIcons: Boolean,
+) {
+ internal val isSpecified: Boolean
+ get() = this !== Unspecified
+
+ public companion object {
+ public val Unspecified: SystemBarStyle = SystemBarStyle(
+ color = Color.Unspecified,
+ darkIcons = false,
+ )
+ }
+}
+
+private inline fun SystemBarStyle.useColor(scrimmed: Boolean = false, block: (Int) -> Unit) {
+ if (color.isSpecified) {
+ val contrastColor = if (scrimmed && color.alpha == 1f) {
+ DarkScrim.compositeOver(color)
+ } else {
+ color
+ }
+ block(contrastColor.toArgb())
+ }
+}
+
+@Suppress("MagicNumber")
+internal fun Color.isLight(): Boolean = luminance() > 0.5f
+
+internal fun scrimOrTransparent(withScrim: Boolean, darkIcons: Boolean): Color = when {
+ withScrim && darkIcons -> LightScrim
+ withScrim && !darkIcons -> DarkScrim
+ else -> Color.Transparent
+}
+
+// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/res/res/color/system_bar_background_semi_transparent.xml
+// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/res/remote_color_resources_res/values/colors.xml;l=67
+internal val DarkScrim = Color(red = 0.1f, green = 0.1f, blue = 0.1f, alpha = 0.5f)
+
+// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/policy/DecorView.java;drc=6ef0f022c333385dba2c294e35b8de544455bf19;l=142
+internal val LightScrim = Color(red = 1.0f, green = 1.0f, blue = 1.0f, alpha = 0.9f)
diff --git a/settings.gradle.kts b/settings.gradle.kts
index fe8f7ec..6570839 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,3 +1,5 @@
+enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
+
pluginManagement {
repositories {
google()
@@ -34,4 +36,5 @@ include(
"lifecycle-livedata-ktx",
"resources-ktx",
"viewbinding-ktx",
+ "compose-ktx",
)