feat: honour the platform's contrast setting, reaching four schemes that were dead code
Phase 1, step 2 of docs/material-design-conformance.md. `Color.kt` has carried medium-
and high-contrast variants of both themes since it was generated -- 156 colour values,
wired into `lightColorScheme`/`darkColorScheme` in `Theme.kt`, and never selected.
`TorchTheme` chose between `darkScheme` and `lightScheme` and nothing else, so a user who
turned contrast up in Accessibility settings got no change at all.
M3's accessibility foundation leads with *honour individuals*: "supporting varying
preferences and choices that allow individuals to address how their changing conditions,
individual knowledge, and varying needs are met." The work to do that was already done and
disconnected.
**The expect/actual boundary moved, because it was in the wrong place.** `themeColorScheme`
took four arguments and did two unrelated jobs -- decide the contrast-free light/dark
scheme, and decide whether to prefer a wallpaper palette. Adding contrast to it would have
meant passing six schemes across the boundary and repeating the selection table in three
actuals. It splits instead into `platformThemeContrast()` and `dynamicColorScheme()`, each
answering one narrow platform question, with the six-way table as a plain function
`appColorScheme(darkTheme, contrast)` in common code. `dynamicColorScheme` returns null
rather than falling back internally so the fallback stays in one place.
**Android reads the setting and listens for changes.** `UiModeManager.getContrast()` is
API 34; the app's minSdk is 26, so below that the answer is Standard. The float is snapped
to the nearest of the platform's three documented positions rather than matched exactly, so
a future finer-grained slider degrades to the closest scheme this app has instead of
falling back to Standard.
The `ContrastChangeListener` is the part that is easy to leave out and matters most. A
contrast change does not restart the activity and does not arrive as a `Configuration`
update, so without it the new setting would take effect on the next cold start -- which is
precisely the case the setting exists for. `context.mainExecutor` rather than
`ContextCompat.getMainExecutor`: it needs API 28, this branch is already gated on 34, and
composeApp does not declare androidx.core -- it only arrives transitively through
activity-compose, which is not a dependency to lean on.
**iOS observes the notification for the same reason** --
`UIAccessibilityDarkerSystemColorsEnabled` plus
`UIAccessibilityDarkerSystemColorsStatusDidChangeNotification`. It is a boolean, not a
slider, so iOS reports High or Standard and never Medium.
**Desktop is honest rather than complete.** Windows publishes high contrast as the
`win.highContrast.on` AWT desktop property and fires a property change when it is toggled,
so that path is real and live. macos "Increase contrast" and the linux desktop equivalents
do not reach AWT, and reading them means a native call per platform, so on those two the
answer is Standard and the file says so. This is the right place for a user-overridable
preference later; a desktop app cannot always see what the desktop was told.
**Verified on an emulator, at the pixel.** API 36, dynamic colour temporarily switched off
(see below for why that is necessary), sampling the `onPrimaryContainer` pixel of the "Skip
for now" label as `settings put secure contrast_level` moved:
standard (0.0) #848484 onPrimaryContainerLight
medium (0.5) #A7A7A7 onPrimaryContainerLightMediumContrast
high (1.0) #D0D0D0 onPrimaryContainerLightHighContrast
The three declared values exactly, and **the app was not restarted between them** -- only
the setting changed, four seconds apart. That is the listener working end to end. The probe
that switched dynamic colour off is reverted in this commit; the emulator's contrast_level
is back at 0.0.
**A finding that came out of the verification, and is not fixed here.** `TorchTheme`
defaults `dynamicColor = true`, and on Android 12+ dynamic colour wins unconditionally --
so on essentially every current Android device **none of the six schemes is used at all**
and the app renders in whatever the user's wallpaper produced. The first screenshot of this
session shows the onboarding screen in Material lavender; switching dynamic colour off
reveals the black-and-gold brand for the first time. Nobody on a modern Android has been
seeing this app's palette.
That is a product decision, not a conformance one, so it is recorded in the plan's "What
this plan does not cover" rather than changed. It does bound what this commit buys: on
Android 14+ with dynamic colour on, contrast is honoured by the platform anyway (the
`system_*` resources shift with it, confirmed on the same emulator -- buttons went
slate-blue to near-black navy). What this commit reaches is Android below 12, Android 12-13,
iOS, and desktop.
**Three new assertions.** `AppColorSchemeSelectionTest` covers the table itself, because its
failure mode is silent and specific: a scheme wired to the wrong cell still renders a
complete, plausible UI, and somebody who turns contrast up and gets the medium scheme back
cannot tell it apart from a high-contrast scheme that is not very high. It asserts each of
the six cells by identity, that all six are distinct objects (a copy-paste leaving two cells
on the same scheme would pass the first test only if it also mislabelled one), and that
`onSurface` on `surface` never *falls* as contrast rises -- the one direction that must
hold, and deliberately not the full monotonicity assertion that ColorSchemeContrastTest
explains is false.
**Not compiled: the iOS actual.** The ios targets are declared only on macos (see
docs/jvm-target.md), so `Theme.ios.kt` is written against the UIKit and Foundation bindings
rather than checked by a compiler. Its file comment says so. The android and jvm actuals of
the same two functions are compiled, and the android one is verified on a device.
**Tests.** 926 pass, 586 jvm over 71 classes and 340 android over 43, up from 920/583/337.
`:composeApp:compileDebugKotlinAndroid` and `:composeApp:compileKotlinJvm` build,
`m3-audit.sh --check` exits 0. The 54 existing `TorchTheme { }` call sites are untouched --
the new parameter is defaulted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,26 +1,79 @@
|
||||
package press.mantra.compose.ui.theme
|
||||
|
||||
import android.app.UiModeManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
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.LocalContext
|
||||
|
||||
/**
|
||||
* Android 14 (API 34) added a three-step contrast setting under Accessibility > Display.
|
||||
* `UiModeManager.getContrast()` reports it as a float, and the platform documents the
|
||||
* three positions as 0.0, 0.5 and 1.0. Values between are treated as the nearer step
|
||||
* rather than rejected -- the API returns a float, so a future finer-grained slider
|
||||
* should degrade to the closest scheme this app has rather than to Standard.
|
||||
*
|
||||
* Below API 34 there is nothing to read and the answer is [ThemeContrast.Standard]. The
|
||||
* app's minSdk is 26.
|
||||
*/
|
||||
@Composable
|
||||
actual fun themeColorScheme(
|
||||
darkTheme: Boolean,
|
||||
dynamicColor: Boolean,
|
||||
darkScheme: ColorScheme,
|
||||
lightScheme: ColorScheme
|
||||
): ColorScheme {
|
||||
return when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
actual fun platformThemeContrast(): ThemeContrast {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return ThemeContrast.Standard
|
||||
|
||||
darkTheme -> darkScheme
|
||||
else -> lightScheme
|
||||
val context = LocalContext.current
|
||||
val uiModeManager = remember(context) {
|
||||
context.getSystemService(Context.UI_MODE_SERVICE) as? UiModeManager
|
||||
} ?: return ThemeContrast.Standard
|
||||
|
||||
var contrast by remember(uiModeManager) {
|
||||
mutableStateOf(contrastStepFor(uiModeManager.contrast))
|
||||
}
|
||||
}
|
||||
|
||||
// The setting can be changed while the app is in the foreground, and unlike a theme
|
||||
// or locale change it does not restart the activity or arrive as a Configuration
|
||||
// update -- so without this listener the new value would only take effect on the
|
||||
// next cold start, which is the case the setting exists for.
|
||||
DisposableEffect(uiModeManager, context) {
|
||||
val listener = UiModeManager.ContrastChangeListener { value ->
|
||||
contrast = contrastStepFor(value)
|
||||
}
|
||||
// context.mainExecutor rather than ContextCompat.getMainExecutor: it needs API 28
|
||||
// and this whole branch is already gated on 34, and it keeps androidx.core off
|
||||
// this file's imports -- composeApp does not declare it, it only arrives
|
||||
// transitively through activity-compose.
|
||||
uiModeManager.addContrastChangeListener(context.mainExecutor, listener)
|
||||
onDispose { uiModeManager.removeContrastChangeListener(listener) }
|
||||
}
|
||||
|
||||
return contrast
|
||||
}
|
||||
|
||||
/** Nearest of the platform's three documented positions. */
|
||||
private fun contrastStepFor(value: Float): ThemeContrast = when {
|
||||
value < 0.25f -> ThemeContrast.Standard
|
||||
value < 0.75f -> ThemeContrast.Medium
|
||||
else -> ThemeContrast.High
|
||||
}
|
||||
|
||||
/**
|
||||
* Material You, from Android 12 (API 31).
|
||||
*
|
||||
* No contrast argument is needed. From API 34 the `android.R.color.system_*` resources
|
||||
* these are built from shift with the contrast setting themselves, so a dynamic scheme
|
||||
* already carries it; passing this app's own contrast on top would apply it twice.
|
||||
*/
|
||||
@Composable
|
||||
actual fun dynamicColorScheme(darkTheme: Boolean, dynamicColor: Boolean): ColorScheme? {
|
||||
if (!dynamicColor || Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return null
|
||||
|
||||
val context = LocalContext.current
|
||||
return if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
@@ -384,19 +384,59 @@ val unspecified_scheme = ColorFamily(
|
||||
Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified
|
||||
)
|
||||
|
||||
/**
|
||||
* How much contrast the person using the device has asked for.
|
||||
*
|
||||
* Not a preference this app invents -- every platform that has the setting owns it, and
|
||||
* `platformThemeContrast` reports it. M3's accessibility foundation puts *honour
|
||||
* individuals* first: "supporting varying preferences and choices that allow individuals
|
||||
* to address how their changing conditions, individual knowledge, and varying needs are
|
||||
* met." The four schemes to answer with were already written out in Color.kt and were,
|
||||
* until this type existed, unreachable.
|
||||
*/
|
||||
enum class ThemeContrast {
|
||||
/** The app's default schemes. */
|
||||
Standard,
|
||||
|
||||
/** Android's 0.5 contrast step. No iOS or desktop equivalent, so never reported there. */
|
||||
Medium,
|
||||
|
||||
/** Android's 1.0 step; iOS "Increase Contrast"; Windows high contrast mode. */
|
||||
High,
|
||||
}
|
||||
|
||||
/**
|
||||
* The scheme to use, before dynamic colour gets a say.
|
||||
*
|
||||
* Kept in common code rather than behind the platform boundary so that all six schemes
|
||||
* are selected from one table. The platform actuals answer two narrow questions instead
|
||||
* -- what contrast was asked for, and whether there is a wallpaper palette to prefer --
|
||||
* which is the part that genuinely differs.
|
||||
*/
|
||||
internal fun appColorScheme(darkTheme: Boolean, contrast: ThemeContrast): ColorScheme =
|
||||
when (contrast) {
|
||||
ThemeContrast.Standard -> if (darkTheme) darkScheme else lightScheme
|
||||
ThemeContrast.Medium ->
|
||||
if (darkTheme) mediumContrastDarkColorScheme else mediumContrastLightColorScheme
|
||||
ThemeContrast.High ->
|
||||
if (darkTheme) highContrastDarkColorScheme else highContrastLightColorScheme
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TorchTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
contrast: ThemeContrast = platformThemeContrast(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable() () -> Unit
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = themeColorScheme(
|
||||
darkTheme,
|
||||
dynamicColor,
|
||||
darkScheme,
|
||||
lightScheme
|
||||
)
|
||||
// Dynamic colour wins when the platform offers it, because it is the user's own
|
||||
// choice and already carries their contrast setting -- on Android 14+ the
|
||||
// `system_*` palette resources shift with it, so `dynamicLightColorScheme` needs
|
||||
// no help from `contrast`. Everywhere else the app's schemes answer, and that is
|
||||
// where `contrast` decides which of the six.
|
||||
val colorScheme = dynamicColorScheme(darkTheme, dynamicColor)
|
||||
?: appColorScheme(darkTheme, contrast)
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
@@ -405,10 +445,21 @@ fun TorchTheme(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The platform's contrast setting, recomposing when it changes.
|
||||
*
|
||||
* Reading it once at startup would be a worse version of honouring it: someone who turns
|
||||
* contrast up because they cannot read the screen in front of them should not have to
|
||||
* find out that the app needs restarting.
|
||||
*/
|
||||
@Composable
|
||||
expect fun themeColorScheme(
|
||||
darkTheme: Boolean,
|
||||
dynamicColor: Boolean,
|
||||
darkScheme: ColorScheme,
|
||||
lightScheme: ColorScheme
|
||||
): ColorScheme
|
||||
expect fun platformThemeContrast(): ThemeContrast
|
||||
|
||||
/**
|
||||
* A wallpaper-derived palette, or `null` where the platform has none.
|
||||
*
|
||||
* Returns `null` rather than falling back internally so that the choice of app scheme --
|
||||
* which now depends on contrast as well as darkness -- stays in one place.
|
||||
*/
|
||||
@Composable
|
||||
expect fun dynamicColorScheme(darkTheme: Boolean, dynamicColor: Boolean): ColorScheme?
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package press.mantra.compose.ui.theme
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotEquals
|
||||
import kotlin.test.assertSame
|
||||
|
||||
/**
|
||||
* The six-way table `appColorScheme` picks from.
|
||||
*
|
||||
* Worth a test because the failure mode is silent and specific: a scheme wired to the
|
||||
* wrong cell still renders a complete, plausible UI. Somebody who turns contrast up and
|
||||
* gets the medium scheme back has no way to tell it apart from a high-contrast scheme
|
||||
* that is simply not very high, and `Color.kt`'s six schemes are 468 near-identical
|
||||
* lines in which a transposed row reads as a typo you would have to be looking for.
|
||||
*
|
||||
* Until this table existed, four of the six schemes were unreachable -- declared in full
|
||||
* and never selected.
|
||||
*/
|
||||
class AppColorSchemeSelectionTest {
|
||||
|
||||
@Test
|
||||
fun `each darkness and contrast pair selects its own scheme`() {
|
||||
assertSame(lightScheme, appColorScheme(darkTheme = false, contrast = ThemeContrast.Standard))
|
||||
assertSame(darkScheme, appColorScheme(darkTheme = true, contrast = ThemeContrast.Standard))
|
||||
|
||||
assertSame(
|
||||
mediumContrastLightColorScheme,
|
||||
appColorScheme(darkTheme = false, contrast = ThemeContrast.Medium),
|
||||
)
|
||||
assertSame(
|
||||
mediumContrastDarkColorScheme,
|
||||
appColorScheme(darkTheme = true, contrast = ThemeContrast.Medium),
|
||||
)
|
||||
|
||||
assertSame(
|
||||
highContrastLightColorScheme,
|
||||
appColorScheme(darkTheme = false, contrast = ThemeContrast.High),
|
||||
)
|
||||
assertSame(
|
||||
highContrastDarkColorScheme,
|
||||
appColorScheme(darkTheme = true, contrast = ThemeContrast.High),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all six schemes are distinct`() {
|
||||
// A copy-paste that left two cells pointing at the same object would satisfy the
|
||||
// table test above only if it also mislabelled one, so this catches the other
|
||||
// half: six declarations that are not six schemes.
|
||||
val schemes = ThemeContrast.entries.flatMap { contrast ->
|
||||
listOf(false, true).map { dark -> "$contrast/${if (dark) "dark" else "light"}" to appColorScheme(dark, contrast) }
|
||||
}
|
||||
|
||||
schemes.forEachIndexed { i, (nameA, a) ->
|
||||
schemes.drop(i + 1).forEach { (nameB, b) ->
|
||||
assertNotEquals(a, b, "$nameA and $nameB are the same scheme")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `raising contrast never lowers the contrast of body text`() {
|
||||
// The one direction that must hold. Individual tonal surfaces legitimately move
|
||||
// the other way -- see ColorSchemeContrastTest for why a full monotonicity
|
||||
// assertion is wrong -- but onSurface against surface is the pair the setting
|
||||
// exists for, and it going backwards would be indefensible.
|
||||
listOf(false, true).forEach { dark ->
|
||||
val standard = appColorScheme(dark, ThemeContrast.Standard)
|
||||
val medium = appColorScheme(dark, ThemeContrast.Medium)
|
||||
val high = appColorScheme(dark, ThemeContrast.High)
|
||||
|
||||
val ratios = listOf(standard, medium, high).map { contrastRatio(it.surface, it.onSurface) }
|
||||
val theme = if (dark) "dark" else "light"
|
||||
|
||||
assertEquals(
|
||||
ratios.sorted(),
|
||||
ratios,
|
||||
"$theme: onSurface on surface does not rise with contrast — $ratios",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,55 @@ package press.mantra.compose.ui.theme
|
||||
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
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 platform.UIKit.UIAccessibilityDarkerSystemColorsEnabled
|
||||
import platform.UIKit.UIAccessibilityDarkerSystemColorsStatusDidChangeNotification
|
||||
import platform.Foundation.NSNotificationCenter
|
||||
import platform.Foundation.NSOperationQueue
|
||||
|
||||
/**
|
||||
* iOS Settings > Accessibility > Display & Text Size > Increase Contrast.
|
||||
*
|
||||
* A boolean, not a three-step slider, so this answers [ThemeContrast.High] or
|
||||
* [ThemeContrast.Standard] and never [ThemeContrast.Medium]. Apple's own contrast work is
|
||||
* mostly done inside its system colours rather than exposed as a level; the one bit it
|
||||
* does expose is `UIAccessibilityDarkerSystemColorsEnabled`.
|
||||
*
|
||||
* **Not compiled on this machine.** The ios targets are declared only on macos (see
|
||||
* docs/jvm-target.md for why the composite build forces that), so this file has been
|
||||
* written against the UIKit and Foundation bindings rather than checked by a compiler.
|
||||
* The Android and jvm actuals of the same two functions are compiled and tested.
|
||||
*/
|
||||
@Composable
|
||||
actual fun themeColorScheme(
|
||||
darkTheme: Boolean,
|
||||
dynamicColor: Boolean,
|
||||
darkScheme: ColorScheme,
|
||||
lightScheme: ColorScheme
|
||||
): ColorScheme {
|
||||
return when {
|
||||
darkTheme -> darkScheme
|
||||
else -> lightScheme
|
||||
actual fun platformThemeContrast(): ThemeContrast {
|
||||
var contrast by remember { mutableStateOf(currentContrast()) }
|
||||
|
||||
// The switch can be flipped while the app is foregrounded, and iOS announces it
|
||||
// rather than restarting anything -- so without this observer the new setting would
|
||||
// wait for the next cold start.
|
||||
DisposableEffect(Unit) {
|
||||
val observer = NSNotificationCenter.defaultCenter.addObserverForName(
|
||||
name = UIAccessibilityDarkerSystemColorsStatusDidChangeNotification,
|
||||
`object` = null,
|
||||
queue = NSOperationQueue.mainQueue,
|
||||
) { _ -> contrast = currentContrast() }
|
||||
|
||||
onDispose { NSNotificationCenter.defaultCenter.removeObserver(observer) }
|
||||
}
|
||||
}
|
||||
|
||||
return contrast
|
||||
}
|
||||
|
||||
private fun currentContrast(): ThemeContrast =
|
||||
if (UIAccessibilityDarkerSystemColorsEnabled()) ThemeContrast.High else ThemeContrast.Standard
|
||||
|
||||
/**
|
||||
* Always `null`: dynamic colour means Material You, an android wallpaper-derived palette
|
||||
* with no iOS counterpart.
|
||||
*/
|
||||
@Composable
|
||||
actual fun dynamicColorScheme(darkTheme: Boolean, dynamicColor: Boolean): ColorScheme? = null
|
||||
|
||||
@@ -2,16 +2,63 @@ package press.mantra.compose.ui.theme
|
||||
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
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 java.awt.Toolkit
|
||||
|
||||
/**
|
||||
* `dynamicColor` is ignored: it means Material You, which reads a wallpaper-derived palette
|
||||
* from the android system and has no desktop counterpart. The app's own schemes are used
|
||||
* whatever the caller asks for.
|
||||
* Desktop has no portable contrast setting, and this reads the one platform that exposes
|
||||
* a usable signal through AWT.
|
||||
*
|
||||
* Windows publishes its high contrast mode as the `win.highContrast.on` desktop property,
|
||||
* which AWT surfaces on that platform and reports as `null` everywhere else. There is no
|
||||
* medium step -- Windows high contrast is a boolean -- so this answers
|
||||
* [ThemeContrast.High] or [ThemeContrast.Standard] and never [ThemeContrast.Medium].
|
||||
*
|
||||
* On linux and macos the answer is always [ThemeContrast.Standard]. macos does have
|
||||
* "Increase contrast" and linux desktops have their own equivalents, but neither reaches
|
||||
* AWT, and reading them means a native call per platform. **Until the app has a settings
|
||||
* screen this is simply unhonoured there**, which is worth knowing rather than papering
|
||||
* over: the setting should ultimately be a preference the user can override anyway,
|
||||
* since a desktop app cannot always see what the desktop was told.
|
||||
*/
|
||||
@Composable
|
||||
actual fun themeColorScheme(
|
||||
darkTheme: Boolean,
|
||||
dynamicColor: Boolean,
|
||||
darkScheme: ColorScheme,
|
||||
lightScheme: ColorScheme
|
||||
): ColorScheme = if (darkTheme) darkScheme else lightScheme
|
||||
actual fun platformThemeContrast(): ThemeContrast {
|
||||
val toolkit = remember { runCatching { Toolkit.getDefaultToolkit() }.getOrNull() }
|
||||
?: return ThemeContrast.Standard
|
||||
|
||||
var contrast by remember(toolkit) { mutableStateOf(windowsHighContrast(toolkit)) }
|
||||
|
||||
// Windows fires a property change when the user toggles high contrast, so the app
|
||||
// does not need restarting. The listener is a no-op on platforms that never publish
|
||||
// the property.
|
||||
DisposableEffect(toolkit) {
|
||||
val listener = java.beans.PropertyChangeListener {
|
||||
contrast = windowsHighContrast(toolkit)
|
||||
}
|
||||
toolkit.addPropertyChangeListener(HIGH_CONTRAST_PROPERTY, listener)
|
||||
onDispose { toolkit.removePropertyChangeListener(HIGH_CONTRAST_PROPERTY, listener) }
|
||||
}
|
||||
|
||||
return contrast
|
||||
}
|
||||
|
||||
private const val HIGH_CONTRAST_PROPERTY = "win.highContrast.on"
|
||||
|
||||
private fun windowsHighContrast(toolkit: Toolkit): ThemeContrast =
|
||||
if (toolkit.getDesktopProperty(HIGH_CONTRAST_PROPERTY) == true) {
|
||||
ThemeContrast.High
|
||||
} else {
|
||||
ThemeContrast.Standard
|
||||
}
|
||||
|
||||
/**
|
||||
* Always `null`: dynamic colour means Material You, which reads a wallpaper-derived
|
||||
* palette from the android system and has no desktop counterpart. The app's own schemes
|
||||
* answer whatever the caller asks for.
|
||||
*/
|
||||
@Composable
|
||||
actual fun dynamicColorScheme(darkTheme: Boolean, dynamicColor: Boolean): ColorScheme? = null
|
||||
|
||||
Reference in New Issue
Block a user