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:
2026-04-12 16:08:12 +05:00
parent 6ad24974e0
commit 1ac3d93f74

View File

@@ -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