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.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
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.drawscope.DrawScope
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.LocalDensity
import androidx.compose.ui.text.font.FontWeight
@@ -77,6 +80,10 @@ class TextSelectionHelper {
var endHandleX 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
fun startSelection(
@@ -199,13 +206,14 @@ class TextSelectionHelper {
magnifier = null
}
fun getCharOffsetFromCoords(x: Int, y: Int): Int {
fun getCharOffsetFromCoords(overlayLocalX: Int, overlayLocalY: Int): Int {
val info = layoutInfo ?: return -1
val localX = x - info.windowX
val localY = y - info.windowY
// overlay-local → text-local: subtract text position relative to overlay
val textLocalX = overlayLocalX - (info.windowX - overlayWindowX)
val textLocalY = overlayLocalY - (info.windowY - overlayWindowY)
val layout = info.layout
val line = layout.getLineForVertical(localY.coerceIn(0, layout.height))
val hx = localX.toFloat().coerceIn(layout.getLineLeft(line), layout.getLineRight(line))
val line = layout.getLineForVertical(textLocalY.toInt().coerceIn(0, layout.height))
val hx = textLocalX.toFloat().coerceIn(layout.getLineLeft(line), layout.getLineRight(line))
return layout.getOffsetForHorizontal(line, hx)
}
@@ -324,7 +332,15 @@ fun TextSelectionOverlay(
val handleInsetPx = with(density) { HandleInset.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)
Canvas(
modifier = Modifier
@@ -387,6 +403,10 @@ fun TextSelectionOverlay(
val layout = info.layout
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 endOffset = helper.selectionEnd.coerceIn(0, text.length)
if (startOffset >= endOffset) return@Canvas
@@ -395,17 +415,17 @@ fun TextSelectionOverlay(
val endLine = layout.getLineForOffset(endOffset)
for (line in startLine..endLine) {
val lineTop = layout.getLineTop(line).toFloat() + info.windowY
val lineBottom = layout.getLineBottom(line).toFloat() + info.windowY
val lineTop = layout.getLineTop(line).toFloat() + offsetY
val lineBottom = layout.getLineBottom(line).toFloat() + offsetY
val left = if (line == startLine) {
layout.getPrimaryHorizontal(startOffset) + info.windowX
layout.getPrimaryHorizontal(startOffset) + offsetX
} else {
layout.getLineLeft(line) + info.windowX
layout.getLineLeft(line) + offsetX
}
val right = if (line == endLine) {
layout.getPrimaryHorizontal(endOffset) + info.windowX
layout.getPrimaryHorizontal(endOffset) + offsetX
} else {
layout.getLineRight(line) + info.windowX
layout.getLineRight(line) + offsetX
}
drawRoundRect(
color = HighlightColor,
@@ -415,10 +435,10 @@ fun TextSelectionOverlay(
)
}
val startHx = layout.getPrimaryHorizontal(startOffset) + info.windowX
val startHy = layout.getLineBottom(startLine).toFloat() + info.windowY
val endHx = layout.getPrimaryHorizontal(endOffset) + info.windowX
val endHy = layout.getLineBottom(endLine).toFloat() + info.windowY
val startHx = layout.getPrimaryHorizontal(startOffset) + offsetX
val startHy = layout.getLineBottom(startLine).toFloat() + offsetY
val endHx = layout.getPrimaryHorizontal(endOffset) + offsetX
val endHy = layout.getLineBottom(endLine).toFloat() + offsetY
helper.startHandleX = startHx
helper.startHandleY = startHy