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