mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-20 09:11:24 +00:00
Make links a static accent and default avatars a flat tint
Links now use the flat accent colour and default avatars the same semi-transparent warm tint as dates and delivery ticks, instead of sampling the sunrise gradient by on-screen position. The sampling machinery did real work on every frame: link colour recomposed as items scrolled, and each default avatar rendered through an offscreen compositing layer to mask the gradient onto the icon. Both are gone — links are a plain span colour, avatars set their icon tint directly. Bubbles keep the position-sampled gradient (they can't be flat-tinted: the wallpaper pattern would show through and hurt text legibility). Non-SIMPLEX themes are unaffected.
This commit is contained in:
+3
-83
@@ -6,13 +6,9 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.CompositingStrategy
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.layout.positionInWindow
|
||||
@@ -31,12 +27,6 @@ import kotlin.math.sin
|
||||
private const val SIMPLEX_TILT_DEG = 20f
|
||||
|
||||
// Per-wallpaper gradient stops live in PresetWallpaper._simplexStops (ChatWallpaper.kt).
|
||||
// Only LINK_STOPS is global — aligned with primary accent, not wallpaper-dependent.
|
||||
private val LINK_STOPS = listOf(
|
||||
0.55f to oklch(0.7993f, 0.1442f, 220.36f), // hue aligned with primary (cyan) — sRGB #00D3F9
|
||||
0.80f to oklch(0.8600f, 0.1000f, 220.36f), // lighter, same hue
|
||||
)
|
||||
|
||||
val LocalSimplexLinkColor = compositionLocalOf<Color?> { null }
|
||||
|
||||
class ChatViewportInfo(
|
||||
@@ -74,23 +64,6 @@ private fun axisEndpoints(winW: Float, winH: Float): Pair<Offset, Offset> {
|
||||
return start to end
|
||||
}
|
||||
|
||||
private fun sampleStops(stops: List<Pair<Float, Color>>, t: Float): Color {
|
||||
if (t <= stops.first().first) return stops.first().second
|
||||
if (t >= stops.last().first) return stops.last().second
|
||||
for (i in 0 until stops.size - 1) {
|
||||
val a = stops[i]; val b = stops[i + 1]
|
||||
if (t in a.first..b.first) {
|
||||
val f = (t - a.first) / (b.first - a.first)
|
||||
return Color(
|
||||
red = a.second.red + f * (b.second.red - a.second.red),
|
||||
green = a.second.green + f * (b.second.green - a.second.green),
|
||||
blue = a.second.blue + f * (b.second.blue - a.second.blue),
|
||||
)
|
||||
}
|
||||
}
|
||||
return stops.last().second
|
||||
}
|
||||
|
||||
enum class SimplexBubbleSlot { Sent, SentQuote, Received, ReceivedQuote }
|
||||
|
||||
/** SimplexStops for a wallpaper, or null when it has none. */
|
||||
@@ -169,37 +142,9 @@ fun Modifier.chatBubbleBackground(
|
||||
)
|
||||
}
|
||||
|
||||
// Tint a single-colour icon (e.g. default ProfileImage placeholder) with the
|
||||
// SIMPLEX axis brush. Drawn as an overlay using BlendMode.SrcAtop so the brush
|
||||
// is masked by the icon's existing alpha — the icon's shape is preserved and
|
||||
// only its colour is replaced by the gradient.
|
||||
fun Modifier.simplexAvatarBrushOverlay(slot: SimplexBubbleSlot): Modifier = composed {
|
||||
if (!isSimplexActive()) return@composed this
|
||||
val viewport = LocalChatViewportInfo.current ?: return@composed this
|
||||
val avatarPos = remember { mutableStateOf(Offset.Zero) }
|
||||
val stops = stopsFor(slot) ?: return@composed this
|
||||
this
|
||||
.onGloballyPositioned { avatarPos.value = it.positionInWindow() }
|
||||
.graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen }
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
val sz = viewport.sizePx.value
|
||||
if (sz.width == 0 || sz.height == 0) return@drawWithContent
|
||||
val (axisStartLocal, axisEndLocal) = axisEndpoints(sz.width.toFloat(), sz.height.toFloat())
|
||||
val viewportOrigin = viewport.originInWindow.value
|
||||
val offset = viewportOrigin - avatarPos.value
|
||||
val brush = Brush.linearGradient(
|
||||
colorStops = stops,
|
||||
start = axisStartLocal + offset,
|
||||
end = axisEndLocal + offset,
|
||||
)
|
||||
drawRect(brush, blendMode = BlendMode.SrcAtop)
|
||||
}
|
||||
}
|
||||
|
||||
// Static tinted-transparency for small text on the SIMPLEX gradient. A semi-transparent
|
||||
// warm colour blends with the gradient at every height, so text stays uniformly legible
|
||||
// without sampling the axis by screen position (bubbles and avatars still sample it).
|
||||
// Static tinted-transparency for small text and default avatars on the SIMPLEX gradient. A
|
||||
// semi-transparent warm colour blends with the gradient at every height, so it stays uniformly
|
||||
// legible without sampling the axis by screen position (only bubbles still sample it).
|
||||
// Composition-scoped: honours the per-chat override / wallpaper preview, and its fallback is
|
||||
// the composition secondary, so non-SIMPLEX chats keep their own (override-aware) colour.
|
||||
@Composable
|
||||
@@ -213,28 +158,3 @@ fun simplexAuthorTint(): Color {
|
||||
if (!isSimplexActive()) return MaterialTheme.colors.onBackground
|
||||
return activeSimplexStops()?.authorTint ?: MaterialTheme.colors.onBackground
|
||||
}
|
||||
|
||||
// Link colour still samples the axis — accent cyan aligned across the whole screen.
|
||||
@Composable
|
||||
fun simplexLinkColor(): Pair<Color, Modifier> = simplexAxisSampledColor(LINK_STOPS, MaterialTheme.colors.primary)
|
||||
|
||||
@Composable
|
||||
private fun simplexAxisSampledColor(stops: List<Pair<Float, Color>>?, fallback: Color): Pair<Color, Modifier> {
|
||||
val isSimplex = isSimplexActive()
|
||||
val viewport = LocalChatViewportInfo.current
|
||||
if (!isSimplex || viewport == null || stops == null) return fallback to Modifier
|
||||
val color = remember(stops) { mutableStateOf(stops.first().second) }
|
||||
val mod = Modifier.onGloballyPositioned { coords ->
|
||||
val sz = viewport.sizePx.value
|
||||
if (sz.width == 0 || sz.height == 0) return@onGloballyPositioned
|
||||
val pos = coords.positionInWindow() - viewport.originInWindow.value
|
||||
val cx = pos.x + coords.size.width / 2f
|
||||
val cy = pos.y + coords.size.height / 2f
|
||||
val (axisStart, axisEnd) = axisEndpoints(sz.width.toFloat(), sz.height.toFloat())
|
||||
val span = (axisEnd - axisStart)
|
||||
val spanLen2 = span.x * span.x + span.y * span.y
|
||||
val t = ((cx - axisStart.x) * span.x + (cy - axisStart.y) * span.y) / spanLen2
|
||||
color.value = sampleStops(stops, t.coerceIn(0f, 1f))
|
||||
}
|
||||
return color.value to mod
|
||||
}
|
||||
|
||||
+5
-9
@@ -2064,10 +2064,8 @@ fun BoxScope.ChatItemsList(
|
||||
}
|
||||
Row(Modifier.graphicsLayer { translationX = selectionOffset.toPx() }) {
|
||||
val member = cItem.chatDir.groupMember
|
||||
val memberAvatarOverlay = if (CurrentColors.value.base == DefaultTheme.SIMPLEX && member.image == null)
|
||||
Modifier.simplexAvatarBrushOverlay(SimplexBubbleSlot.ReceivedQuote) else Modifier
|
||||
Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }.then(memberAvatarOverlay)) {
|
||||
MemberImage(member)
|
||||
Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) {
|
||||
MemberImage(member, color = if (CurrentColors.value.base == DefaultTheme.SIMPLEX && member.image == null) simplexSecondaryTint() else MaterialTheme.colors.secondaryVariant)
|
||||
}
|
||||
Box(modifier = Modifier.padding(top = 2.dp, start = 4.dp).chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)) {
|
||||
ChatItemViewShortHand(cItem, itemSeparation, range, false, dismissState.offset.value)
|
||||
@@ -2299,9 +2297,7 @@ fun BoxScope.ChatItemsList(
|
||||
.padding(top = DEFAULT_PADDING_HALF)
|
||||
.let { if (CurrentColors.value.base == DefaultTheme.SIMPLEX) it else it.background(MaterialTheme.appColors.receivedMessage) }
|
||||
) {
|
||||
Box(if (CurrentColors.value.base == DefaultTheme.SIMPLEX && chatInfo.image == null) Modifier.simplexAvatarBrushOverlay(SimplexBubbleSlot.ReceivedQuote) else Modifier) {
|
||||
ChatInfoImage(chatInfo, size = alertProfileImageSize, iconColor = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f))
|
||||
}
|
||||
ChatInfoImage(chatInfo, size = alertProfileImageSize, iconColor = if (CurrentColors.value.base == DefaultTheme.SIMPLEX && chatInfo.image == null) simplexSecondaryTint() else MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f))
|
||||
val bannerBadge = chatInfo.nameBadge
|
||||
val uriHandler = LocalUriHandler.current
|
||||
Text(
|
||||
@@ -2820,8 +2816,8 @@ private suspend fun preloadItemsAfter(
|
||||
val MEMBER_IMAGE_SIZE: Dp = 37.dp
|
||||
|
||||
@Composable
|
||||
fun MemberImage(member: GroupMember) {
|
||||
MemberProfileImage(MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier, member, backgroundColor = MaterialTheme.colors.background)
|
||||
fun MemberImage(member: GroupMember, color: Color = MaterialTheme.colors.secondaryVariant) {
|
||||
MemberProfileImage(MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier, member, color = color, backgroundColor = MaterialTheme.colors.background)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+6
-10
@@ -55,16 +55,12 @@ fun CIFileView(
|
||||
) {
|
||||
val isDefaultTint = color == (if (isInDarkTheme()) FileDark else FileLight)
|
||||
val isSimplex = CurrentColors.value.base == DefaultTheme.SIMPLEX
|
||||
val outerOverlay = if (isSimplex && isDefaultTint)
|
||||
Modifier.simplexAvatarBrushOverlay(SimplexBubbleSlot.ReceivedQuote) else Modifier
|
||||
Box(outerOverlay.fillMaxSize()) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_draft_filled),
|
||||
stringResource(MR.strings.icon_descr_file),
|
||||
Modifier.fillMaxSize(),
|
||||
tint = color
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
painterResource(MR.images.ic_draft_filled),
|
||||
stringResource(MR.strings.icon_descr_file),
|
||||
Modifier.fillMaxSize(),
|
||||
tint = if (isSimplex && isDefaultTint) simplexSecondaryTint() else color
|
||||
)
|
||||
if (innerIcon != null) {
|
||||
Icon(
|
||||
innerIcon,
|
||||
|
||||
+1
-5
@@ -53,15 +53,11 @@ fun CIGroupInvitationView(
|
||||
if (action && !inProgress.value) if (chatIncognito) Indigo else MaterialTheme.colors.primary
|
||||
else if (isInDarkTheme()) FileDark else FileLight
|
||||
val isSimplex = CurrentColors.value.base == DefaultTheme.SIMPLEX
|
||||
val avatarOverlayMod = if (isSimplex && groupInvitation.groupProfile.image == null)
|
||||
Modifier.simplexAvatarBrushOverlay(SimplexBubbleSlot.ReceivedQuote) else Modifier
|
||||
|
||||
Row(
|
||||
Modifier.defaultMinSize(minWidth = 220.dp)
|
||||
) {
|
||||
Box(avatarOverlayMod) {
|
||||
ProfileImage(size = 54.dp, image = groupInvitation.groupProfile.image, icon = MR.images.ic_supervised_user_circle_filled, color = iconColor)
|
||||
}
|
||||
ProfileImage(size = 54.dp, image = groupInvitation.groupProfile.image, icon = MR.images.ic_supervised_user_circle_filled, color = if (isSimplex && groupInvitation.groupProfile.image == null) simplexSecondaryTint() else iconColor)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Column(
|
||||
Modifier.defaultMinSize(minHeight = 54.dp),
|
||||
|
||||
+2
-4
@@ -207,13 +207,11 @@ fun FramedItemView(
|
||||
val transparentBackground = (ci.content.msgContent is MsgContent.MCImage || ci.content.msgContent is MsgContent.MCVideo) &&
|
||||
!ci.meta.isLive && ci.content.text.isEmpty() && ci.quotedItem == null && ci.meta.itemForwarded == null
|
||||
|
||||
val (linkColor, linkModifier) = simplexLinkColor()
|
||||
Box(Modifier
|
||||
.clipChatItem(ci, tailVisible, revealed = true)
|
||||
.chatBubbleBackground(sent = sent, isQuote = false, transparent = transparentBackground)
|
||||
.then(linkModifier)) {
|
||||
.chatBubbleBackground(sent = sent, isQuote = false, transparent = transparentBackground)) {
|
||||
var metaColor: Color? = null
|
||||
CompositionLocalProvider(LocalSimplexLinkColor provides linkColor) {
|
||||
CompositionLocalProvider(LocalSimplexLinkColor provides MaterialTheme.colors.primary) {
|
||||
Box(contentAlignment = Alignment.BottomEnd) {
|
||||
val chatItemTail = remember { appPreferences.chatItemTail.state }
|
||||
val style = shapeStyle(ci, chatItemTail.value, tailVisible, true)
|
||||
|
||||
Reference in New Issue
Block a user