feat: добавить TextSelectionOverlay — highlight, handles, drag interaction

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 15:03:35 +05:00
parent 988896c080
commit 419761e34d

View File

@@ -7,11 +7,25 @@ import android.text.Layout
import android.view.HapticFeedbackConstants
import android.view.View
import android.widget.Toast
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
data class LayoutInfo(
val layout: Layout,
@@ -34,6 +48,17 @@ class TextSelectionHelper {
private set
var handleViewProgress by mutableFloatStateOf(0f)
private set
var movingHandle by mutableStateOf(false)
private set
var movingHandleStart by mutableStateOf(false)
private set
private var movingOffsetX = 0f
private var movingOffsetY = 0f
var startHandleX by mutableFloatStateOf(0f)
var startHandleY by mutableFloatStateOf(0f)
var endHandleX by mutableFloatStateOf(0f)
var endHandleY by mutableFloatStateOf(0f)
val isInSelectionMode: Boolean get() = isActive && selectionStart >= 0 && selectionEnd > selectionStart
@@ -89,6 +114,26 @@ class TextSelectionHelper {
selectionEnd = newEnd
}
fun beginHandleDrag(isStart: Boolean, touchX: Float, touchY: Float) {
movingHandle = true
movingHandleStart = isStart
movingOffsetX = if (isStart) startHandleX - touchX else endHandleX - touchX
movingOffsetY = if (isStart) startHandleY - touchY else endHandleY - touchY
}
fun moveHandle(touchX: Float, touchY: Float) {
if (!movingHandle) return
val x = (touchX + movingOffsetX).toInt()
val y = (touchY + movingOffsetY).toInt()
val offset = getCharOffsetFromCoords(x, y)
if (offset < 0) return
if (movingHandleStart) updateSelectionStart(offset) else updateSelectionEnd(offset)
}
fun endHandleDrag() {
movingHandle = false
}
fun getCharOffsetFromCoords(x: Int, y: Int): Int {
val info = layoutInfo ?: return -1
val localX = x - info.windowX
@@ -128,5 +173,154 @@ class TextSelectionHelper {
layoutInfo = null
isActive = false
handleViewProgress = 0f
movingHandle = false
}
}
private val HandleSize = 22.dp
private val HandleInset = 8.dp
private val HighlightCorner = 6.dp
private val HighlightColor = PrimaryBlue.copy(alpha = 0.3f)
private val HandleColor = PrimaryBlue
@Composable
fun TextSelectionOverlay(
helper: TextSelectionHelper,
modifier: Modifier = Modifier
) {
if (!helper.isInSelectionMode) return
val density = LocalDensity.current
val handleSizePx = with(density) { HandleSize.toPx() }
val handleInsetPx = with(density) { HandleInset.toPx() }
val highlightCornerPx = with(density) { HighlightCorner.toPx() }
Box(modifier = modifier.fillMaxSize()) {
Canvas(
modifier = Modifier
.fillMaxSize()
.pointerInput(helper.isActive) {
if (!helper.isActive) return@pointerInput
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
val change = event.changes.firstOrNull() ?: continue
when {
change.pressed && !helper.movingHandle -> {
val x = change.position.x
val y = change.position.y
val startRect = Rect(
helper.startHandleX - handleSizePx / 2 - handleInsetPx,
helper.startHandleY - handleInsetPx,
helper.startHandleX + handleSizePx / 2 + handleInsetPx,
helper.startHandleY + handleSizePx + handleInsetPx
)
val endRect = Rect(
helper.endHandleX - handleSizePx / 2 - handleInsetPx,
helper.endHandleY - handleInsetPx,
helper.endHandleX + handleSizePx / 2 + handleInsetPx,
helper.endHandleY + handleSizePx + handleInsetPx
)
when {
startRect.contains(Offset(x, y)) -> {
helper.beginHandleDrag(isStart = true, x, y)
change.consume()
}
endRect.contains(Offset(x, y)) -> {
helper.beginHandleDrag(isStart = false, x, y)
change.consume()
}
else -> {
helper.clear()
}
}
}
change.pressed && helper.movingHandle -> {
helper.moveHandle(change.position.x, change.position.y)
change.consume()
}
!change.pressed && helper.movingHandle -> {
helper.endHandleDrag()
change.consume()
}
}
}
}
}
) {
val info = helper.layoutInfo ?: return@Canvas
val layout = info.layout
val text = info.text
val startOffset = helper.selectionStart.coerceIn(0, text.length)
val endOffset = helper.selectionEnd.coerceIn(0, text.length)
if (startOffset >= endOffset) return@Canvas
val startLine = layout.getLineForOffset(startOffset)
val endLine = layout.getLineForOffset(endOffset)
for (line in startLine..endLine) {
val lineTop = layout.getLineTop(line).toFloat() + info.windowY
val lineBottom = layout.getLineBottom(line).toFloat() + info.windowY
val left = if (line == startLine) {
layout.getPrimaryHorizontal(startOffset) + info.windowX
} else {
layout.getLineLeft(line) + info.windowX
}
val right = if (line == endLine) {
layout.getPrimaryHorizontal(endOffset) + info.windowX
} else {
layout.getLineRight(line) + info.windowX
}
drawRoundRect(
color = HighlightColor,
topLeft = Offset(left, lineTop),
size = Size(right - left, lineBottom - lineTop),
cornerRadius = CornerRadius(highlightCornerPx)
)
}
val startHx = layout.getPrimaryHorizontal(startOffset) + info.windowX
val startHy = layout.getLineBottom(startLine).toFloat() + info.windowY
val endHx = layout.getPrimaryHorizontal(endOffset) + info.windowX
val endHy = layout.getLineBottom(endLine).toFloat() + info.windowY
helper.startHandleX = startHx
helper.startHandleY = startHy
helper.endHandleX = endHx
helper.endHandleY = endHy
drawStartHandle(startHx, startHy, handleSizePx)
drawEndHandle(endHx, endHy, handleSizePx)
}
}
}
private fun DrawScope.drawStartHandle(x: Float, y: Float, size: Float) {
val half = size / 2f
drawCircle(
color = HandleColor,
radius = half,
center = Offset(x, y + half)
)
drawRect(
color = HandleColor,
topLeft = Offset(x, y),
size = Size(half, half)
)
}
private fun DrawScope.drawEndHandle(x: Float, y: Float, size: Float) {
val half = size / 2f
drawCircle(
color = HandleColor,
radius = half,
center = Offset(x, y + half)
)
drawRect(
color = HandleColor,
topLeft = Offset(x - half, y),
size = Size(half, half)
)
}