mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-15 08:05:49 +00:00
toolbar: respect per-chat theme override
The toolbar background was always computed from the global CurrentColors state flow, so a chat with a custom theme override (its own wallpaper or color scheme) showed a toolbar tinted to the global Appearance scheme rather than the chat's. On desktop two-pane the chatlist toolbar sat next to the chat toolbar in different colors; on Android the chat info and chat-customize modals kept the global tint above and below the chat-themed content. Expose the current theme as a new LocalActiveTheme composition local, provided by SimpleXTheme (global scope) and SimpleXThemeOverride (chat scope). panelBackgroundColor reads everything — both the wallpaper-hue tint and the elevation fallback — from this local, so it tints to whatever scope it renders in. A small ActiveChatThemeProvider helper propagates the active chat's effective theme to UI surfaces that sit outside the chat's SimpleXThemeOverride: the chatlist column on desktop, and the fullscreen modal stack on Android. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -203,7 +203,9 @@ fun MainScreen() {
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
AndroidWrapInCallLayout {
|
||||
ModalManager.fullscreen.showInView()
|
||||
ActiveChatThemeProvider {
|
||||
ModalManager.fullscreen.showInView()
|
||||
}
|
||||
}
|
||||
SwitchingUsersView()
|
||||
}
|
||||
@@ -422,45 +424,68 @@ fun EndPartOfScreen() {
|
||||
ModalManager.end.showInView()
|
||||
}
|
||||
|
||||
// Toolbars rendered alongside or on behalf of the active chat (the chatlist column
|
||||
// in desktop two-pane, modals opened from a chat on Android) need to share that
|
||||
// chat's theme. Wrap such UI surfaces with this so panelBackgroundColor() reads
|
||||
// the right tint via LocalActiveTheme.
|
||||
@Composable
|
||||
private fun ActiveChatThemeProvider(content: @Composable () -> Unit) {
|
||||
val theme by CurrentColors.collectAsState()
|
||||
val chatId by chatModel.chatId
|
||||
val activeChat = chatId?.let { id -> chatModel.chats.value.firstOrNull { it.chatInfo.id == id } }
|
||||
val effectiveTheme = remember(activeChat?.chatInfo, theme) {
|
||||
val perChatTheme = when (val ci = activeChat?.chatInfo) {
|
||||
is ChatInfo.Direct -> ci.contact.uiThemes?.preferredMode(!theme.colors.isLight)
|
||||
is ChatInfo.Group -> ci.groupInfo.uiThemes?.preferredMode(!theme.colors.isLight)
|
||||
else -> null
|
||||
}
|
||||
if (perChatTheme != null) ThemeManager.currentColors(null, perChatTheme, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||
else theme
|
||||
}
|
||||
CompositionLocalProvider(LocalActiveTheme provides effectiveTheme, content = content)
|
||||
}
|
||||
|
||||
// Spec: spec/client/navigation.md#DesktopScreen
|
||||
@Composable
|
||||
fun DesktopScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
|
||||
Box(Modifier.width(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) {
|
||||
StartPartOfScreen(userPickerState)
|
||||
tryOrShowError("UserPicker", error = {}) {
|
||||
UserPicker(chatModel, userPickerState, setPerformLA = AppLock::setPerformLA)
|
||||
ActiveChatThemeProvider {
|
||||
Box(Modifier.width(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) {
|
||||
StartPartOfScreen(userPickerState)
|
||||
tryOrShowError("UserPicker", error = {}) {
|
||||
UserPicker(chatModel, userPickerState, setPerformLA = AppLock::setPerformLA)
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) {
|
||||
ModalManager.start.showInView()
|
||||
SwitchingUsersView()
|
||||
}
|
||||
Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier).clipToBounds()) {
|
||||
Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) {
|
||||
CenterPartOfScreen()
|
||||
Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) {
|
||||
ModalManager.start.showInView()
|
||||
SwitchingUsersView()
|
||||
}
|
||||
if (ModalManager.end.hasModalsOpen()) {
|
||||
VerticalDivider()
|
||||
Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier).clipToBounds()) {
|
||||
Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) {
|
||||
CenterPartOfScreen()
|
||||
}
|
||||
if (ModalManager.end.hasModalsOpen()) {
|
||||
VerticalDivider()
|
||||
}
|
||||
Box(Modifier.widthIn(max = DEFAULT_END_MODAL_WIDTH * fontSizeSqrtMultiplier).clipToBounds()) {
|
||||
EndPartOfScreen()
|
||||
}
|
||||
}
|
||||
Box(Modifier.widthIn(max = DEFAULT_END_MODAL_WIDTH * fontSizeSqrtMultiplier).clipToBounds()) {
|
||||
EndPartOfScreen()
|
||||
if (userPickerState.collectAsState().value.isVisible() || (ModalManager.start.hasModalsOpen && !ModalManager.center.hasModalsOpen)) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)
|
||||
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = {
|
||||
if (chatModel.centerPanelBackgroundClickHandler == null || chatModel.centerPanelBackgroundClickHandler?.invoke() == false) {
|
||||
ModalManager.start.closeModals()
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier))
|
||||
ModalManager.fullscreen.showInView()
|
||||
}
|
||||
if (userPickerState.collectAsState().value.isVisible() || (ModalManager.start.hasModalsOpen && !ModalManager.center.hasModalsOpen)) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)
|
||||
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = {
|
||||
if (chatModel.centerPanelBackgroundClickHandler == null || chatModel.centerPanelBackgroundClickHandler?.invoke() == false) {
|
||||
ModalManager.start.closeModals()
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier))
|
||||
ModalManager.fullscreen.showInView()
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+4
-6
@@ -3,7 +3,6 @@ package chat.simplex.common.ui.theme
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.colorspace.ColorSpaces
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
@@ -57,13 +56,12 @@ val NoteFolderIconColor: Color @Composable get() = MaterialTheme.appColors.prima
|
||||
* BLACK and SIMPLEX themes are not tinted (BLACK keeps pure dark, SIMPLEX has its own custom panel). */
|
||||
@Composable
|
||||
fun panelBackgroundColor(): Color {
|
||||
return currentWallpaperPanelTint()
|
||||
?: MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f)
|
||||
val state = LocalActiveTheme.current
|
||||
return currentWallpaperPanelTint(state)
|
||||
?: state.colors.background.mixWith(state.colors.onBackground, 0.97f)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun currentWallpaperPanelTint(): Color? {
|
||||
val state = CurrentColors.collectAsState().value
|
||||
private fun currentWallpaperPanelTint(state: ThemeManager.ActiveTheme): Color? {
|
||||
val type = state.wallpaper.type as? WallpaperType.Preset ?: return null
|
||||
val preset = PresetWallpaper.from(type.filename) ?: return null
|
||||
val hue = preset.hue(state.base)
|
||||
|
||||
@@ -792,6 +792,7 @@ expect fun isSystemInDarkTheme(): Boolean
|
||||
|
||||
internal val LocalAppColors = staticCompositionLocalOf { LightColorPaletteApp }
|
||||
internal val LocalAppWallpaper = staticCompositionLocalOf { AppWallpaper() }
|
||||
internal val LocalActiveTheme = staticCompositionLocalOf { CurrentColors.value }
|
||||
|
||||
val MaterialTheme.appColors: AppColors
|
||||
@Composable
|
||||
@@ -873,6 +874,7 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
|
||||
LocalContentColor provides MaterialTheme.colors.onBackground,
|
||||
LocalAppColors provides rememberedAppColors,
|
||||
LocalAppWallpaper provides rememberedWallpaper,
|
||||
LocalActiveTheme provides theme,
|
||||
LocalDensity provides density,
|
||||
content = content
|
||||
)
|
||||
@@ -901,6 +903,7 @@ fun SimpleXThemeOverride(theme: ThemeManager.ActiveTheme, content: @Composable (
|
||||
LocalContentColor provides MaterialTheme.colors.onBackground,
|
||||
LocalAppColors provides rememberedAppColors,
|
||||
LocalAppWallpaper provides rememberedWallpaper,
|
||||
LocalActiveTheme provides theme,
|
||||
content = content)
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user