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:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user