From 9bbef911c0b0c3f2949240d2fea1cbc256a25127 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 24 Jul 2026 17:11:16 +0200 Subject: [PATCH] fix(components): match full-screen picker bar icons to the runtime theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Compose Dialog owns its own Window whose status/navigation-bar icon appearance is seeded from the XML theme, not from the host activity's runtime edge-to-edge state. With decorFitsSystemWindows = false the FullScreenPicker draws under the bars, exposing icons that don't match the active theme — dark icons on a dark picker (near-invisible), and the mirror-image nav-bar bug in light theme. Expose the resolved light/dark decision from FloretExpressiveTheme as LocalFloretDarkTheme and drive the dialog window's isAppearanceLight{Status,Navigation}Bars from it, so the appearance tracks the in-app light/dark choice even when it diverges from the system night resource qualifier. Co-Authored-By: Claude Opus 4.8 (1M context) --- components/build.gradle.kts | 2 ++ .../floret/components/OptionPicker.kt | 22 +++++++++++++--- .../jeanlucmakiola/floret/identity/Theme.kt | 25 ++++++++++++++----- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/components/build.gradle.kts b/components/build.gradle.kts index 111df3d..7f9119f 100644 --- a/components/build.gradle.kts +++ b/components/build.gradle.kts @@ -39,6 +39,8 @@ dependencies { // core-locale backs the language-picker recipe (AppLanguage). implementation(project(":core-locale")) + implementation(libs.androidx.core.ktx) + implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.ui) implementation(libs.androidx.foundation) diff --git a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/OptionPicker.kt b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/OptionPicker.kt index ec09e48..6c0d984 100644 --- a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/OptionPicker.kt +++ b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/OptionPicker.kt @@ -13,6 +13,8 @@ import androidx.compose.ui.platform.LocalView import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogWindowProvider +import androidx.core.view.WindowCompat +import de.jeanlucmakiola.floret.identity.LocalFloretDarkTheme /** * Full-screen scaffold for selection pickers: a full-bleed [Dialog] that reuses @@ -41,14 +43,28 @@ fun FullScreenPicker( decorFitsSystemWindows = false, ), ) { - // The dialog window pans by default when the keyboard opens, which — + // A Compose Dialog owns a separate android.view.Window whose bar-icon + // appearance is seeded from the XML theme, not from the activity window's + // runtime edge-to-edge state — so with decorFitsSystemWindows = false it + // draws edge-to-edge under bars whose icons don't match the active theme + // (dark icons on a dark picker). Drive the appearance from the runtime + // [LocalFloretDarkTheme] so it tracks the in-app light/dark choice even + // when that diverges from the system night qualifier. + // + // Same window also pans by default when the keyboard opens, which — // combined with the content's own imePadding — leaves a fixed black gap // above the keyboard. Switch it to ADJUST_NOTHING so the window stays // full-screen and imePadding alone lifts the focused field. val view = LocalView.current + val darkTheme = LocalFloretDarkTheme.current SideEffect { - (view.parent as? DialogWindowProvider)?.window - ?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) + (view.parent as? DialogWindowProvider)?.window?.let { window -> + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) + WindowCompat.getInsetsController(window, view).apply { + isAppearanceLightStatusBars = !darkTheme + isAppearanceLightNavigationBars = !darkTheme + } + } } CollapsingScaffold( title = title, diff --git a/identity/src/main/kotlin/de/jeanlucmakiola/floret/identity/Theme.kt b/identity/src/main/kotlin/de/jeanlucmakiola/floret/identity/Theme.kt index c8178d9..bce727d 100644 --- a/identity/src/main/kotlin/de/jeanlucmakiola/floret/identity/Theme.kt +++ b/identity/src/main/kotlin/de/jeanlucmakiola/floret/identity/Theme.kt @@ -10,8 +10,19 @@ import androidx.compose.material3.Typography import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.platform.LocalContext +/** + * The resolved light/dark decision the current [FloretExpressiveTheme] is running + * with, carried down the tree so components can align window chrome (e.g. status- + * and navigation-bar icon appearance) with the *runtime* theme rather than the + * system night resource qualifier — the two diverge whenever an app overrides + * light/dark from settings. Defaults to `false` (light) outside a Floret theme. + */ +val LocalFloretDarkTheme = staticCompositionLocalOf { false } + /** * The family's Material 3 Expressive theme factory. Honours system light/dark * and Material You dynamic colour (API 31+), falling back to the app-supplied @@ -43,10 +54,12 @@ fun FloretExpressiveTheme( else -> lightScheme } - MaterialExpressiveTheme( - colorScheme = colorScheme, - typography = typography, - motionScheme = MotionScheme.standard(), - content = content, - ) + CompositionLocalProvider(LocalFloretDarkTheme provides darkTheme) { + MaterialExpressiveTheme( + colorScheme = colorScheme, + typography = typography, + motionScheme = MotionScheme.standard(), + content = content, + ) + } }