mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-27 22:34:51 +00:00
android, desktop: refine RTL fix and add design doc
Surgical refinements on top of the initial RTL fix, plus a design doc
in plans/.
- chatItemShape (ChatItemView.kt): extract `val tailOnRight = ...` so
the predicate names what the conditional does. Comment trimmed to
the single load-bearing invariant ("default path draws tail at
bottom-left") since the rest is implied by the variable name.
- SubscriptionStatusIcon.kt: hoist `val iconModifier =
modifier.mirrorIfRtl()` once at the top of the composable and reuse
it at all five Icon call sites. Calling mirrorIfRtl five times or
once produces the same Modifier within one composition.
- GoToItemInnerButton (ChatItemView.kt): add `mirror: Boolean = false`
parameter and pass `mirror = true` at the directional call site
(ic_arrow_forward, "go to forwarded source"). The other call site
(ic_search) keeps the default and is unchanged.
- Modifiers.kt, FramedItemView.kt: drop two explanatory comments that
restated framework-primitive behavior (mirrorIfRtl's own name, and
the documented behavior of placeRelative).
plans/2026-05-13-rtl-layout-issues.md documents the three independent
mechanisms behind the fix (bubble shape, app-bar slot placement,
asymmetric drawables), the bubble-tail XOR predicate, and four
considered-and-rejected alternatives.
This commit is contained in:
+6
-7
@@ -238,7 +238,7 @@ fun ChatItemView(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GoToItemInnerButton(alignStart: Boolean, icon: ImageResource, iconSize: Dp = 22.dp, parentActivated: State<Boolean>, onClick: () -> Unit) {
|
||||
fun GoToItemInnerButton(alignStart: Boolean, icon: ImageResource, iconSize: Dp = 22.dp, parentActivated: State<Boolean>, mirror: Boolean = false, onClick: () -> Unit) {
|
||||
val buttonInteractionSource = remember { MutableInteractionSource() }
|
||||
val buttonHovered = buttonInteractionSource.collectIsHoveredAsState()
|
||||
val buttonPressed = buttonInteractionSource.collectIsPressedAsState()
|
||||
@@ -273,7 +273,7 @@ fun ChatItemView(
|
||||
.size(22.dp),
|
||||
interactionSource = buttonInteractionSource
|
||||
) {
|
||||
Icon(painterResource(icon), null, Modifier.size(iconSize), tint = iconTint)
|
||||
Icon(painterResource(icon), null, Modifier.size(iconSize).then(if (mirror) Modifier.mirrorIfRtl() else Modifier), tint = iconTint)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@ fun ChatItemView(
|
||||
}
|
||||
}
|
||||
} else if (chatTypeApiIdMsgId != null) {
|
||||
GoToItemInnerButton(alignStart, MR.images.ic_arrow_forward, 22.dp, parentActivated) {
|
||||
GoToItemInnerButton(alignStart, MR.images.ic_arrow_forward, 22.dp, parentActivated, mirror = true) {
|
||||
val (chatType, apiId, msgId) = chatTypeApiIdMsgId
|
||||
withBGApi {
|
||||
openChat(secondaryChatsCtx = null, rhId, chatType, apiId, msgId)
|
||||
@@ -1283,10 +1283,9 @@ private fun chatItemShape(roundness: Float, density: Density, tailVisible: Boole
|
||||
quadraticBezierTo(bubbleInitialX, 0f, bubbleInitialX + rx, 0f) // Top-left corner
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
// Default path draws the tail at the bottom-left corner.
|
||||
val tailOnRight = sent != (layoutDirection == LayoutDirection.Rtl)
|
||||
if (tailOnRight) {
|
||||
val matrix = Matrix()
|
||||
matrix.scale(-1f, 1f)
|
||||
this.transform(matrix)
|
||||
|
||||
-2
@@ -592,8 +592,6 @@ 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) {
|
||||
// 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))
|
||||
|
||||
-2
@@ -12,8 +12,6 @@ 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
|
||||
|
||||
+6
-5
@@ -14,28 +14,29 @@ fun SubscriptionStatusIcon(
|
||||
variableValue: Float,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val iconModifier = modifier.mirrorIfRtl()
|
||||
@Composable
|
||||
fun ZeroIcon() {
|
||||
Icon(painterResource(MR.images.ic_radiowaves_up_forward_4_bar), null, tint = color.copy(alpha = 0.33f), modifier = modifier.mirrorIfRtl())
|
||||
Icon(painterResource(MR.images.ic_radiowaves_up_forward_4_bar), null, tint = color.copy(alpha = 0.33f), modifier = iconModifier)
|
||||
}
|
||||
|
||||
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.mirrorIfRtl())
|
||||
Icon(painterResource(MR.images.ic_radiowaves_up_forward_1_bar), null, tint = color, modifier = iconModifier)
|
||||
}
|
||||
|
||||
variableValue > 0.25f && variableValue <= 0.5f -> Box {
|
||||
ZeroIcon()
|
||||
Icon(painterResource(MR.images.ic_radiowaves_up_forward_2_bar), null, tint = color, modifier = modifier.mirrorIfRtl())
|
||||
Icon(painterResource(MR.images.ic_radiowaves_up_forward_2_bar), null, tint = color, modifier = iconModifier)
|
||||
}
|
||||
|
||||
variableValue > 0.5f && variableValue <= 0.75f -> Box {
|
||||
ZeroIcon()
|
||||
Icon(painterResource(MR.images.ic_radiowaves_up_forward_3_bar), null, tint = color, modifier = modifier.mirrorIfRtl())
|
||||
Icon(painterResource(MR.images.ic_radiowaves_up_forward_3_bar), null, tint = color, modifier = iconModifier)
|
||||
}
|
||||
|
||||
else -> Icon(painterResource(MR.images.ic_radiowaves_up_forward_4_bar), null, tint = color, modifier = modifier.mirrorIfRtl())
|
||||
else -> Icon(painterResource(MR.images.ic_radiowaves_up_forward_4_bar), null, tint = color, modifier = iconModifier)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user