Introduce "Black" theme and refactor theme handling

* Add `Theme.Black` to the `Theme` enum and update `isDark()` to include it.
* Refactor `ElementTheme` to accept a `Theme` object instead of a `darkTheme` boolean, allowing for more specific color mapping (e.g., setting `bgCanvasDefault` to `Color.Black` for the Black theme).
* Update `AdvancedSettingsPresenter` and `AdvancedSettingsState` to include "Black" as a user-selectable theme option.
* Adjust `ElementThemeApp` and `MaterialTextPreview` to handle the expanded theme selection.
* Add `ElementPreviewBlack` and update existing preview components to support the new theme.
* Update Konsist tests to ensure `@PreviewsDayNight` annotated functions don't have "BlackPreview" suffix.
This commit is contained in:
Timur Gilfanov 2026-03-23 15:26:16 +04:00
parent fdbe518db3
commit a96c146d30
23 changed files with 121 additions and 36 deletions

View file

@ -38,6 +38,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.theme.Theme
import io.element.android.compound.tokens.generated.CompoundIcons
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -62,7 +63,7 @@ internal fun IconsCompoundPreviewRtl() = ElementTheme {
@Preview(widthDp = 730, heightDp = 1920)
@Composable
internal fun IconsCompoundPreviewDark() = ElementTheme(darkTheme = true) {
internal fun IconsCompoundPreviewDark() = ElementTheme(theme = Theme.Dark) {
IconsCompoundPreview()
}

View file

@ -19,6 +19,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.theme.Theme
import io.element.android.compound.tokens.generated.compoundColorsHcDark
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf
@ -65,7 +66,7 @@ internal fun CompoundSemanticColorsLightHc() = ElementTheme(
@Preview(heightDp = 2000)
@Composable
internal fun CompoundSemanticColorsDark() = ElementTheme(darkTheme = true) {
internal fun CompoundSemanticColorsDark() = ElementTheme(theme = Theme.Dark) {
Surface {
Column(
modifier = Modifier.padding(16.dp),
@ -85,7 +86,7 @@ internal fun CompoundSemanticColorsDark() = ElementTheme(darkTheme = true) {
@Preview(heightDp = 2000)
@Composable
internal fun CompoundSemanticColorsDarkHc() = ElementTheme(
darkTheme = true,
theme = Theme.Dark,
compoundDark = compoundColorsHcDark,
) {
Surface {

View file

@ -65,7 +65,7 @@ internal fun AvatarColorsPreviewLight() {
@Preview
@Composable
internal fun AvatarColorsPreviewDark() {
ElementTheme(darkTheme = true) {
ElementTheme(theme = Theme.Dark) {
val chunks = avatarColors().chunked(4)
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
for (chunk in chunks) {

View file

@ -77,10 +77,10 @@ internal val LocalCompoundColors = staticCompositionLocalOf { compoundColorsLigh
/**
* Sets up the theme for the application, or a part of it.
*
* @param darkTheme whether to use the dark theme or not. If `true`, the dark theme will be used.
* @param theme the [Theme] to use. Defaults to [Theme.Dark] or [Theme.Light] based on the system setting.
* @param applySystemBarsUpdate whether to update the system bars color scheme or not when the theme changes. It's `true` by default.
* This is specially useful when you want to apply an alternate theme to a part of the app but don't want it to affect the system bars.
* @param lightStatusBar whether to use a light status bar color scheme or not. By default, it's the opposite of [darkTheme].
* @param lightStatusBar whether to use a light status bar color scheme or not. By default, it's `true` for light themes and `false` for dark ones.
* @param dynamicColor whether to enable MaterialYou or not. It's `false` by default.
* @param compoundLight the [SemanticColors] to use in light theme.
* @param compoundDark the [SemanticColors] to use in dark theme.
@ -91,9 +91,9 @@ internal val LocalCompoundColors = staticCompositionLocalOf { compoundColorsLigh
*/
@Composable
fun ElementTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
theme: Theme = if (isSystemInDarkTheme()) Theme.Dark else Theme.Light,
applySystemBarsUpdate: Boolean = true,
lightStatusBar: Boolean = !darkTheme,
lightStatusBar: Boolean = !theme.isDark(),
// true to enable MaterialYou
dynamicColor: Boolean = false,
compoundLight: SemanticColors = compoundColorsLight,
@ -103,8 +103,13 @@ fun ElementTheme(
typography: Typography = compoundTypography,
content: @Composable () -> Unit,
) {
val darkTheme = theme.isDark()
val currentCompoundColor = when {
darkTheme -> compoundDark
darkTheme -> if (theme == Theme.Black) {
compoundDark.copy(bgCanvasDefault = Color.Black)
} else {
compoundDark
}
else -> compoundLight
}
@ -113,7 +118,11 @@ fun ElementTheme(
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> materialColorsDark
darkTheme -> if (theme == Theme.Black) {
currentCompoundColor.toMaterialColorScheme()
} else {
materialColorsDark
}
else -> materialColorsLight
}
@ -130,7 +139,7 @@ fun ElementTheme(
if (applySystemBarsUpdate) {
val activity = LocalActivity.current as? ComponentActivity
LaunchedEffect(statusBarColorScheme, darkTheme, lightStatusBar) {
LaunchedEffect(statusBarColorScheme, theme, lightStatusBar) {
activity?.enableEdgeToEdge(
// For Status bar use the background color of the app
statusBarStyle = SystemBarStyle.auto(

View file

@ -51,7 +51,7 @@ fun ForcedDarkElementTheme(
}
}
ElementTheme(
darkTheme = true,
theme = Theme.Dark,
compoundLight = colors.light,
compoundDark = colors.dark,
lightStatusBar = lightStatusBar,

View file

@ -36,11 +36,15 @@ internal fun MaterialTextPreview() = Row(
) {
MaterialPreview(
modifier = Modifier.weight(1f),
darkTheme = false,
theme = Theme.Light,
)
MaterialPreview(
modifier = Modifier.weight(1f),
darkTheme = true,
theme = Theme.Dark,
)
MaterialPreview(
modifier = Modifier.weight(1f),
theme = Theme.Black,
)
}
@ -52,7 +56,7 @@ private data class Model(
@Composable
private fun MaterialPreview(
darkTheme: Boolean,
theme: Theme,
modifier: Modifier = Modifier,
) = Column(modifier = modifier) {
Text(
@ -60,13 +64,13 @@ private fun MaterialPreview(
.fillMaxWidth()
.padding(8.dp),
textAlign = TextAlign.Center,
text = if (darkTheme) "Dark" else "Light",
text = theme.name,
color = Color.Black,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
)
ElementTheme(
darkTheme = darkTheme,
theme = theme,
) {
Column(
modifier = Modifier.fillMaxSize()

View file

@ -50,7 +50,7 @@ internal fun ColorsSchemeLightHcPreview() = ElementTheme(
@Preview(heightDp = 1200)
@Composable
internal fun ColorsSchemeDarkPreview() = ElementTheme(
darkTheme = true,
theme = Theme.Dark,
) {
ColorsSchemePreview(
Color.White,
@ -62,7 +62,7 @@ internal fun ColorsSchemeDarkPreview() = ElementTheme(
@Preview(heightDp = 1200)
@Composable
internal fun ColorsSchemeDarkHcPreview() = ElementTheme(
darkTheme = true,
theme = Theme.Dark,
compoundDark = compoundColorsHcDark,
) {
ColorsSchemePreview(
@ -71,3 +71,15 @@ internal fun ColorsSchemeDarkHcPreview() = ElementTheme(
ElementTheme.materialColors,
)
}
@Preview(heightDp = 1200)
@Composable
internal fun ColorsSchemeBlackPreview() = ElementTheme(
theme = Theme.Black
) {
ColorsSchemePreview(
Color.White,
Color.Black,
ElementTheme.materialColors,
)
}

View file

@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.map
enum class Theme {
System,
Dark,
Black,
Light,
}
@ -23,7 +24,7 @@ enum class Theme {
fun Theme.isDark(): Boolean {
return when (this) {
Theme.System -> isSystemInDarkTheme()
Theme.Dark -> true
Theme.Dark, Theme.Black -> true
Theme.Light -> false
}
}

View file

@ -19,6 +19,7 @@ import io.element.android.compound.previews.IconsCompoundPreviewRtl
import io.element.android.compound.previews.IconsPreview
import io.element.android.compound.screenshot.utils.screenshotFile
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.theme.Theme
import io.element.android.compound.tokens.generated.CompoundIcons
import kotlinx.collections.immutable.toImmutableList
import org.junit.Test
@ -56,7 +57,7 @@ class CompoundIconTest {
val content: List<@Composable ColumnScope.() -> Unit> = CompoundIcons.all.map {
@Composable { Icon(imageVector = it, contentDescription = null) }
}
ElementTheme(darkTheme = true) {
ElementTheme(theme = Theme.Dark) {
IconsPreview(
title = "Compound Vector Icons",
content = content.toImmutableList()

View file

@ -24,6 +24,7 @@ import com.github.takahirom.roborazzi.captureRoboImage
import io.element.android.compound.previews.ColorsSchemePreview
import io.element.android.compound.screenshot.utils.screenshotFile
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.theme.Theme
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@ -51,7 +52,7 @@ class MaterialYouThemeTest {
}
}
captureRoboImage(file = screenshotFile("MaterialYou Theme - Dark.png")) {
ElementTheme(dynamicColor = true, darkTheme = true) {
ElementTheme(dynamicColor = true, theme = Theme.Dark) {
Surface {
Column(
modifier = Modifier.padding(16.dp),