fix: правильные координаты text selection — window→overlay-local конвертация
Root cause: overlay Canvas рисует в локальных координатах, но LayoutInfo возвращает позицию в window coordinates. Разница = position status bar, toolbar, и parent padding → highlight смещался вниз. Фикс: - onGloballyPositioned на overlay Box → знаем overlayWindowX/Y - Canvas: offsetX/Y = info.windowX - overlayWindowX (window→local) - getCharOffsetFromCoords: overlay-local → text-local через ту же delta - Handle positions теперь в overlay-local координатах → drag работает Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -34,6 +35,8 @@ import androidx.compose.ui.geometry.Size
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.layout.positionInWindow
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -77,6 +80,10 @@ class TextSelectionHelper {
|
|||||||
var endHandleX by mutableFloatStateOf(0f)
|
var endHandleX by mutableFloatStateOf(0f)
|
||||||
var endHandleY by mutableFloatStateOf(0f)
|
var endHandleY by mutableFloatStateOf(0f)
|
||||||
|
|
||||||
|
// Overlay position in window — set by TextSelectionOverlay
|
||||||
|
var overlayWindowX = 0f
|
||||||
|
var overlayWindowY = 0f
|
||||||
|
|
||||||
val isInSelectionMode: Boolean get() = isActive && selectionStart >= 0 && selectionEnd > selectionStart
|
val isInSelectionMode: Boolean get() = isActive && selectionStart >= 0 && selectionEnd > selectionStart
|
||||||
|
|
||||||
fun startSelection(
|
fun startSelection(
|
||||||
@@ -199,13 +206,14 @@ class TextSelectionHelper {
|
|||||||
magnifier = null
|
magnifier = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCharOffsetFromCoords(x: Int, y: Int): Int {
|
fun getCharOffsetFromCoords(overlayLocalX: Int, overlayLocalY: Int): Int {
|
||||||
val info = layoutInfo ?: return -1
|
val info = layoutInfo ?: return -1
|
||||||
val localX = x - info.windowX
|
// overlay-local → text-local: subtract text position relative to overlay
|
||||||
val localY = y - info.windowY
|
val textLocalX = overlayLocalX - (info.windowX - overlayWindowX)
|
||||||
|
val textLocalY = overlayLocalY - (info.windowY - overlayWindowY)
|
||||||
val layout = info.layout
|
val layout = info.layout
|
||||||
val line = layout.getLineForVertical(localY.coerceIn(0, layout.height))
|
val line = layout.getLineForVertical(textLocalY.toInt().coerceIn(0, layout.height))
|
||||||
val hx = localX.toFloat().coerceIn(layout.getLineLeft(line), layout.getLineRight(line))
|
val hx = textLocalX.toFloat().coerceIn(layout.getLineLeft(line), layout.getLineRight(line))
|
||||||
return layout.getOffsetForHorizontal(line, hx)
|
return layout.getOffsetForHorizontal(line, hx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,7 +332,15 @@ fun TextSelectionOverlay(
|
|||||||
val handleInsetPx = with(density) { HandleInset.toPx() }
|
val handleInsetPx = with(density) { HandleInset.toPx() }
|
||||||
val highlightCornerPx = with(density) { HighlightCorner.toPx() }
|
val highlightCornerPx = with(density) { HighlightCorner.toPx() }
|
||||||
|
|
||||||
Box(modifier = modifier.fillMaxSize()) {
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.onGloballyPositioned { coords ->
|
||||||
|
val pos = coords.positionInWindow()
|
||||||
|
helper.overlayWindowX = pos.x
|
||||||
|
helper.overlayWindowY = pos.y
|
||||||
|
}
|
||||||
|
) {
|
||||||
FloatingToolbarPopup(helper = helper)
|
FloatingToolbarPopup(helper = helper)
|
||||||
Canvas(
|
Canvas(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -387,6 +403,10 @@ fun TextSelectionOverlay(
|
|||||||
val layout = info.layout
|
val layout = info.layout
|
||||||
val text = info.text
|
val text = info.text
|
||||||
|
|
||||||
|
// Convert window coords to overlay-local coords
|
||||||
|
val offsetX = info.windowX - helper.overlayWindowX
|
||||||
|
val offsetY = info.windowY - helper.overlayWindowY
|
||||||
|
|
||||||
val startOffset = helper.selectionStart.coerceIn(0, text.length)
|
val startOffset = helper.selectionStart.coerceIn(0, text.length)
|
||||||
val endOffset = helper.selectionEnd.coerceIn(0, text.length)
|
val endOffset = helper.selectionEnd.coerceIn(0, text.length)
|
||||||
if (startOffset >= endOffset) return@Canvas
|
if (startOffset >= endOffset) return@Canvas
|
||||||
@@ -395,17 +415,17 @@ fun TextSelectionOverlay(
|
|||||||
val endLine = layout.getLineForOffset(endOffset)
|
val endLine = layout.getLineForOffset(endOffset)
|
||||||
|
|
||||||
for (line in startLine..endLine) {
|
for (line in startLine..endLine) {
|
||||||
val lineTop = layout.getLineTop(line).toFloat() + info.windowY
|
val lineTop = layout.getLineTop(line).toFloat() + offsetY
|
||||||
val lineBottom = layout.getLineBottom(line).toFloat() + info.windowY
|
val lineBottom = layout.getLineBottom(line).toFloat() + offsetY
|
||||||
val left = if (line == startLine) {
|
val left = if (line == startLine) {
|
||||||
layout.getPrimaryHorizontal(startOffset) + info.windowX
|
layout.getPrimaryHorizontal(startOffset) + offsetX
|
||||||
} else {
|
} else {
|
||||||
layout.getLineLeft(line) + info.windowX
|
layout.getLineLeft(line) + offsetX
|
||||||
}
|
}
|
||||||
val right = if (line == endLine) {
|
val right = if (line == endLine) {
|
||||||
layout.getPrimaryHorizontal(endOffset) + info.windowX
|
layout.getPrimaryHorizontal(endOffset) + offsetX
|
||||||
} else {
|
} else {
|
||||||
layout.getLineRight(line) + info.windowX
|
layout.getLineRight(line) + offsetX
|
||||||
}
|
}
|
||||||
drawRoundRect(
|
drawRoundRect(
|
||||||
color = HighlightColor,
|
color = HighlightColor,
|
||||||
@@ -415,10 +435,10 @@ fun TextSelectionOverlay(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val startHx = layout.getPrimaryHorizontal(startOffset) + info.windowX
|
val startHx = layout.getPrimaryHorizontal(startOffset) + offsetX
|
||||||
val startHy = layout.getLineBottom(startLine).toFloat() + info.windowY
|
val startHy = layout.getLineBottom(startLine).toFloat() + offsetY
|
||||||
val endHx = layout.getPrimaryHorizontal(endOffset) + info.windowX
|
val endHx = layout.getPrimaryHorizontal(endOffset) + offsetX
|
||||||
val endHy = layout.getLineBottom(endLine).toFloat() + info.windowY
|
val endHy = layout.getLineBottom(endLine).toFloat() + offsetY
|
||||||
|
|
||||||
helper.startHandleX = startHx
|
helper.startHandleX = startHx
|
||||||
helper.startHandleY = startHy
|
helper.startHandleY = startHy
|
||||||
|
|||||||
Reference in New Issue
Block a user