mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-14 23:25:33 +00:00
android, desktop: fix several RTL layout issues
Fixes https://github.com/simplex-chat/simplex-chat/issues/5448 The original report documents four user-visible regressions under an RTL locale (Arabic, Persian, Hebrew): chat bubble tails on the wrong side, chat header call/menu buttons not mirrored, settings back arrow pointing the wrong way, and the chat list profile avatar / signal icon laid out in their LTR positions. This commit addresses all of them and a handful of related directional-icon mirroring sites discovered during the audit. - Chat bubble tail direction: chatItemShape now reads layoutDirection from the GenericShape lambda and conditions the matrix mirror on sent != isRtl, so sent-bubble tails point toward the user's profile and received-bubble tails toward the contact's profile under both LTR and RTL. The bubble-tail paddings in FramedItemView and ChatView already use direction-aware start/end and now align with the mirrored shape automatically. - App-bar slot mirroring: CenteredRowLayout in FramedItemView switches from place(...) to placeRelative(...) so the navigation icon and the action buttons swap visual sides under RTL. Both the chat header (call/menu buttons) and the chat list header (profile avatar, signal icon) flow through this layout, so one fix covers both. - Asymmetric directional drawables: add Modifier.mirrorIfRtl() (Modifier.scale(-1f, 1f) when LocalLayoutDirection.current is Rtl) and apply it to the back arrow in NavigationButtonBack, every variant of the SubscriptionStatusIcon waves, the WhatsNewView pagination arrows, the back arrow and submenu chevron in CommandsMenuView, the back arrow in OnboardingCards and ImageFullScreenView.desktop, the share-profile chevron in NewChatView, and the sent-via-proxy arrow in CIMetaView and ChatItemInfoView. DropdownMenu offsets in ChatView (DpOffset(-width, ...)) are left unchanged: DropdownMenuPositionProvider already negates the content offset under RTL, so menus anchor and open correctly once CenteredRowLayout is fixed.
This commit is contained in:
+1
@@ -359,6 +359,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
Icon(
|
||||
painterResource(MR.images.ic_arrow_forward),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.mirrorIfRtl(),
|
||||
tint = CurrentColors.value.colors.secondary
|
||||
)
|
||||
}
|
||||
|
||||
+3
@@ -25,6 +25,7 @@ import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
|
||||
import chat.simplex.common.views.chat.group.*
|
||||
import chat.simplex.common.views.chat.item.sendCommandMsg
|
||||
import chat.simplex.common.views.helpers.commandMenuAnimSpec
|
||||
import chat.simplex.common.views.helpers.mirrorIfRtl
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -126,6 +127,7 @@ fun CommandsMenuView(
|
||||
Icon(
|
||||
painterResource(MR.images.ic_arrow_back_ios_new),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.mirrorIfRtl(),
|
||||
tint = MaterialTheme.colors.secondary
|
||||
)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING_HALF))
|
||||
@@ -205,6 +207,7 @@ fun CommandsMenuView(
|
||||
Icon(
|
||||
painterResource(MR.images.ic_chevron_right),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.mirrorIfRtl(),
|
||||
tint = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
|
||||
+2
-1
@@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.isInDarkTheme
|
||||
import chat.simplex.common.views.helpers.mirrorIfRtl
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@@ -87,7 +88,7 @@ private fun CIMetaText(
|
||||
}
|
||||
if (showViaProxy && meta.sentViaProxy == true) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Icon(painterResource(MR.images.ic_arrow_forward), null, Modifier.height(17.dp), tint = MaterialTheme.colors.secondary)
|
||||
Icon(painterResource(MR.images.ic_arrow_forward), null, Modifier.height(17.dp).mirrorIfRtl(), tint = MaterialTheme.colors.secondary)
|
||||
}
|
||||
if (showStatus) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
|
||||
+5
-2
@@ -1231,7 +1231,7 @@ fun Modifier.clipChatItem(chatItem: ChatItem? = null, tailVisible: Boolean = fal
|
||||
return this.clip(shape)
|
||||
}
|
||||
|
||||
private fun chatItemShape(roundness: Float, density: Density, tailVisible: Boolean, sent: Boolean = false): GenericShape = GenericShape { size, _ ->
|
||||
private fun chatItemShape(roundness: Float, density: Density, tailVisible: Boolean, sent: Boolean = false): GenericShape = GenericShape { size, layoutDirection ->
|
||||
val (msgTailWidth, msgBubbleMaxRadius) = with(density) { Pair(msgTailWidthDp.toPx(), msgBubbleMaxRadius.toPx()) }
|
||||
val width = size.width
|
||||
val height = size.height
|
||||
@@ -1283,7 +1283,10 @@ private fun chatItemShape(roundness: Float, density: Density, tailVisible: Boole
|
||||
quadraticBezierTo(bubbleInitialX, 0f, bubbleInitialX + rx, 0f) // Top-left corner
|
||||
}
|
||||
|
||||
if (sent) {
|
||||
// The default path draws the tail at the bottom-left corner. We mirror the path so the tail
|
||||
// ends up on the side closer to the message author's profile in chat: right for sent in LTR,
|
||||
// left for received in LTR — and the opposite under RTL where chat sides are mirrored.
|
||||
if (sent != (layoutDirection == LayoutDirection.Rtl)) {
|
||||
val matrix = Matrix()
|
||||
matrix.scale(-1f, 1f)
|
||||
this.transform(matrix)
|
||||
|
||||
+5
-3
@@ -592,9 +592,11 @@ fun CenteredRowLayout(
|
||||
val second = measureable[1].measure(constraints.copy(minWidth = 0, minHeight = 0, maxWidth = (constraints.maxWidth - first.measuredWidth - third.measuredWidth).coerceAtLeast(0)))
|
||||
// Limit width for every other element to width of important element and height for a sum of all elements.
|
||||
layout(constraints.maxWidth, constraints.maxHeight) {
|
||||
first.place(0, ((constraints.maxHeight - first.measuredHeight) / 2).coerceAtLeast(0))
|
||||
second.place((constraints.maxWidth - second.measuredWidth) / 2, ((constraints.maxHeight - second.measuredHeight) / 2).coerceAtLeast(0))
|
||||
third.place(constraints.maxWidth - third.measuredWidth, ((constraints.maxHeight - third.measuredHeight) / 2).coerceAtLeast(0))
|
||||
// placeRelative mirrors x under RTL so the leading slot (first) and trailing slot (third)
|
||||
// swap visual sides automatically when the locale flips.
|
||||
first.placeRelative(0, ((constraints.maxHeight - first.measuredHeight) / 2).coerceAtLeast(0))
|
||||
second.placeRelative((constraints.maxWidth - second.measuredWidth) / 2, ((constraints.maxHeight - second.measuredHeight) / 2).coerceAtLeast(0))
|
||||
third.placeRelative(constraints.maxWidth - third.measuredWidth, ((constraints.maxHeight - third.measuredHeight) / 2).coerceAtLeast(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -128,7 +128,7 @@ fun CallAppBar(
|
||||
fun NavigationButtonBack(onButtonClicked: (() -> Unit)?, tintColor: Color = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, height: Dp = 24.dp) {
|
||||
IconButton(onButtonClicked ?: {}, enabled = onButtonClicked != null) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_arrow_back_ios_new), stringResource(MR.strings.back), Modifier.height(height), tint = tintColor
|
||||
painterResource(MR.images.ic_arrow_back_ios_new), stringResource(MR.strings.back), Modifier.height(height).mirrorIfRtl(), tint = tintColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -5,12 +5,19 @@ import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
// Horizontally flips an asymmetric directional drawable (back/forward arrows, chevrons, signal
|
||||
// waves) when the layout direction is RTL. Apply to the Icon's modifier at each call site.
|
||||
@Composable
|
||||
fun Modifier.mirrorIfRtl(): Modifier =
|
||||
if (LocalLayoutDirection.current == LayoutDirection.Rtl) this.scale(scaleX = -1f, scaleY = 1f) else this
|
||||
|
||||
fun Modifier.badgeLayout() =
|
||||
layout { measurable, constraints ->
|
||||
val placeable = measurable.measure(constraints)
|
||||
|
||||
+5
-5
@@ -16,26 +16,26 @@ fun SubscriptionStatusIcon(
|
||||
) {
|
||||
@Composable
|
||||
fun ZeroIcon() {
|
||||
Icon(painterResource(MR.images.ic_radiowaves_up_forward_4_bar), null, tint = color.copy(alpha = 0.33f), modifier = modifier)
|
||||
Icon(painterResource(MR.images.ic_radiowaves_up_forward_4_bar), null, tint = color.copy(alpha = 0.33f), modifier = modifier.mirrorIfRtl())
|
||||
}
|
||||
|
||||
when {
|
||||
variableValue <= 0f -> ZeroIcon()
|
||||
variableValue > 0f && variableValue <= 0.25f -> Box {
|
||||
ZeroIcon()
|
||||
Icon(painterResource(MR.images.ic_radiowaves_up_forward_1_bar), null, tint = color, modifier = modifier)
|
||||
Icon(painterResource(MR.images.ic_radiowaves_up_forward_1_bar), null, tint = color, modifier = modifier.mirrorIfRtl())
|
||||
}
|
||||
|
||||
variableValue > 0.25f && variableValue <= 0.5f -> Box {
|
||||
ZeroIcon()
|
||||
Icon(painterResource(MR.images.ic_radiowaves_up_forward_2_bar), null, tint = color, modifier = modifier)
|
||||
Icon(painterResource(MR.images.ic_radiowaves_up_forward_2_bar), null, tint = color, modifier = modifier.mirrorIfRtl())
|
||||
}
|
||||
|
||||
variableValue > 0.5f && variableValue <= 0.75f -> Box {
|
||||
ZeroIcon()
|
||||
Icon(painterResource(MR.images.ic_radiowaves_up_forward_3_bar), null, tint = color, modifier = modifier)
|
||||
Icon(painterResource(MR.images.ic_radiowaves_up_forward_3_bar), null, tint = color, modifier = modifier.mirrorIfRtl())
|
||||
}
|
||||
|
||||
else -> Icon(painterResource(MR.images.ic_radiowaves_up_forward_4_bar), null, tint = color, modifier = modifier)
|
||||
else -> Icon(painterResource(MR.images.ic_radiowaves_up_forward_4_bar), null, tint = color, modifier = modifier.mirrorIfRtl())
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -568,6 +568,7 @@ private fun InviteView(rhId: Long?, connLinkInvitation: CreatedConnLink, contact
|
||||
painter = painterResource(MR.images.ic_arrow_forward_ios),
|
||||
contentDescription = stringResource(MR.strings.new_chat_share_profile),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier.mirrorIfRtl(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -263,7 +263,7 @@ private fun BackButton(modifier: Modifier = Modifier, color: Color = MaterialThe
|
||||
painterResource(MR.images.ic_arrow_back_ios_new),
|
||||
contentDescription = stringResource(MR.strings.back),
|
||||
tint = color,
|
||||
modifier = Modifier.height(24.dp)
|
||||
modifier = Modifier.height(24.dp).mirrorIfRtl()
|
||||
)
|
||||
Text(stringResource(MR.strings.back), color = color)
|
||||
}
|
||||
|
||||
+2
-2
@@ -112,7 +112,7 @@ fun ModalData.WhatsNewView(updatedConditions: Boolean = false, viaSettings: Bool
|
||||
.clickable { currentVersion.value = prev }
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Icon(painterResource(MR.images.ic_arrow_back_ios_new), "previous", tint = MaterialTheme.colors.primary)
|
||||
Icon(painterResource(MR.images.ic_arrow_back_ios_new), "previous", Modifier.mirrorIfRtl(), tint = MaterialTheme.colors.primary)
|
||||
Text(versionDescriptions[prev].version, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,7 @@ fun ModalData.WhatsNewView(updatedConditions: Boolean = false, viaSettings: Bool
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Text(versionDescriptions[next].version, color = MaterialTheme.colors.primary)
|
||||
Icon(painterResource(MR.images.ic_arrow_forward_ios), "next", tint = MaterialTheme.colors.primary)
|
||||
Icon(painterResource(MR.images.ic_arrow_forward_ios), "next", Modifier.mirrorIfRtl(), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -11,6 +11,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.helpers.getBitmapFromByteArray
|
||||
import chat.simplex.common.views.helpers.mirrorIfRtl
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
@@ -33,7 +34,7 @@ actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier, close: (
|
||||
Box(Modifier.fillMaxSize().padding(bottom = 50.dp)) {
|
||||
SurfaceFromPlayer(player, modifier)
|
||||
IconButton(onClick = close, Modifier.padding(top = 5.dp)) {
|
||||
Icon(painterResource(MR.images.ic_arrow_back_ios_new), null, Modifier.size(30.dp), tint = MaterialTheme.colors.primary)
|
||||
Icon(painterResource(MR.images.ic_arrow_back_ios_new), null, Modifier.size(30.dp).mirrorIfRtl(), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
Controls(player)
|
||||
|
||||
Reference in New Issue
Block a user