android, desktop: fix chat item long-press menu and ripple shape (#6997)

* android, desktop: fix chat item long-press menu and ripple shape

clipChatItem clipped the bubble shape with Modifier.clip. Modifier.clip
of the bubble GenericShape mis-hit-tests its path on very tall items, so
long-press on the lower part of a long message did not reach
combinedClickable and the context menu did not open (#6991); on desktop
the same clip also left the press ripple rendered as a rectangle.

Clip the bubble GenericShape in the draw pass (drawWithCache + clipPath)
instead: drawing is clipped identically, the press ripple included, with
no effect on hit-test. The RoundRect shape (tail disabled) hit-tests
correctly and keeps Modifier.clip.

Fixes #6991

* plans: justify chat item long-press and ripple shape fix

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
Narasimha-sc
2026-06-07 23:46:59 +01:00
committed by GitHub
co-authored by Evgeny Poberezkin
parent bf905eb545
commit 7548fdae3b
2 changed files with 88 additions and 5 deletions
@@ -12,8 +12,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.*
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.*
@@ -1224,12 +1226,25 @@ fun Modifier.clipChatItem(chatItem: ChatItem? = null, tailVisible: Boolean = fal
val style = shapeStyle(chatItem, chatItemTail.value, tailVisible, revealed)
val cornerRoundness = chatItemRoundness.value.coerceIn(0f, 1f)
val shape = when (style) {
is ShapeStyle.Bubble -> chatItemShape(cornerRoundness, LocalDensity.current, style.tailVisible, chatItem?.chatDir?.sent == true)
is ShapeStyle.RoundRect -> RoundedCornerShape(style.radius * cornerRoundness)
return when (style) {
is ShapeStyle.Bubble -> {
// Modifier.clip of the bubble GenericShape mis-hit-tests its path on very tall
// items, dropping long-press on the lower part of the bubble (issue #6991). Clip
// in the draw pass instead — drawing is clipped identically (the press ripple
// included), with no effect on hit-test.
val shape = chatItemShape(cornerRoundness, LocalDensity.current, style.tailVisible, chatItem?.chatDir?.sent == true)
this.drawWithCache {
val path = Path().apply {
addOutline(shape.createOutline(size, layoutDirection, this@drawWithCache))
}
onDrawWithContent {
clipPath(path) { this@onDrawWithContent.drawContent() }
}
}
}
// RoundRect hit-tests correctly — no bug here, keep the antialiased Modifier.clip.
is ShapeStyle.RoundRect -> this.clip(RoundedCornerShape(style.radius * cornerRoundness))
}
return this.clip(shape)
}
private fun chatItemShape(roundness: Float, density: Density, tailVisible: Boolean, sent: Boolean = false): GenericShape = GenericShape { size, _ ->