feat: добавить TextSelectionHelper — core state, word snap, char offset

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 14:44:46 +05:00
parent b57e48fe20
commit 988896c080

View File

@@ -0,0 +1,132 @@
package com.rosetta.messenger.ui.chats.components
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.text.Layout
import android.view.HapticFeedbackConstants
import android.view.View
import android.widget.Toast
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
data class LayoutInfo(
val layout: Layout,
val windowX: Int,
val windowY: Int,
val text: CharSequence
)
class TextSelectionHelper {
var selectionStart by mutableIntStateOf(-1)
private set
var selectionEnd by mutableIntStateOf(-1)
private set
var selectedMessageId by mutableStateOf<String?>(null)
private set
var layoutInfo by mutableStateOf<LayoutInfo?>(null)
private set
var isActive by mutableStateOf(false)
private set
var handleViewProgress by mutableFloatStateOf(0f)
private set
val isInSelectionMode: Boolean get() = isActive && selectionStart >= 0 && selectionEnd > selectionStart
fun startSelection(
messageId: String,
info: LayoutInfo,
touchX: Int,
touchY: Int,
view: View?
) {
val layout = info.layout
val localX = touchX - info.windowX
val localY = touchY - info.windowY
val line = layout.getLineForVertical(localY)
val hx = localX.toFloat().coerceIn(layout.getLineLeft(line), layout.getLineRight(line))
val offset = layout.getOffsetForHorizontal(line, hx)
val text = info.text
var start = offset
var end = offset
while (start > 0 && Character.isLetterOrDigit(text[start - 1])) start--
while (end < text.length && Character.isLetterOrDigit(text[end])) end++
if (start == end && end < text.length) end++
selectedMessageId = messageId
layoutInfo = info
selectionStart = start
selectionEnd = end
isActive = true
handleViewProgress = 1f
view?.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
}
fun updateSelectionStart(charOffset: Int) {
if (!isActive) return
val text = layoutInfo?.text ?: return
var newStart = charOffset.coerceIn(0, text.length)
while (newStart > 0 && Character.isLetterOrDigit(text[newStart - 1])) newStart--
if (newStart >= selectionEnd) return
selectionStart = newStart
}
fun updateSelectionEnd(charOffset: Int) {
if (!isActive) return
val text = layoutInfo?.text ?: return
var newEnd = charOffset.coerceIn(0, text.length)
while (newEnd < text.length && Character.isLetterOrDigit(text[newEnd])) newEnd++
if (newEnd <= selectionStart) return
selectionEnd = newEnd
}
fun getCharOffsetFromCoords(x: Int, y: Int): Int {
val info = layoutInfo ?: return -1
val localX = x - info.windowX
val localY = y - info.windowY
val layout = info.layout
val line = layout.getLineForVertical(localY.coerceIn(0, layout.height))
val hx = localX.toFloat().coerceIn(layout.getLineLeft(line), layout.getLineRight(line))
return layout.getOffsetForHorizontal(line, hx)
}
fun getSelectedText(): CharSequence? {
if (!isInSelectionMode) return null
val text = layoutInfo?.text ?: return null
val start = selectionStart.coerceIn(0, text.length)
val end = selectionEnd.coerceIn(start, text.length)
return text.subSequence(start, end)
}
fun copySelectedText(context: Context) {
val selectedText = getSelectedText() ?: return
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("selected_text", selectedText))
Toast.makeText(context, "Copied", Toast.LENGTH_SHORT).show()
clear()
}
fun selectAll() {
val text = layoutInfo?.text ?: return
selectionStart = 0
selectionEnd = text.length
}
fun clear() {
selectionStart = -1
selectionEnd = -1
selectedMessageId = null
layoutInfo = null
isActive = false
handleViewProgress = 0f
}
}