From 979656c2d0fbfb16fae66fded7e0c2af154c23dc Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Wed, 3 Jun 2026 22:47:06 -0400 Subject: [PATCH] Fix cursor Y positioning logic in editor --- internal/editor/state.go | 102 ++++++++++++++++++++++++--------------- 1 file changed, 63 insertions(+), 39 deletions(-) diff --git a/internal/editor/state.go b/internal/editor/state.go index 589eb4d..1b6a8f7 100644 --- a/internal/editor/state.go +++ b/internal/editor/state.go @@ -457,60 +457,84 @@ func SetCursorFromPoint(x, y float64) { return } - // 1. Find the best line based on y - bestLineY := layout.Y[0] - minYDist := 1e9 - for _, yVal := range layout.Y { - yDist := float64(yVal) - y - if yDist < 0 { - yDist = -yDist - } - if yDist < minYDist { - minYDist = yDist - bestLineY = yVal + lineHeight := float64(EditorLineHeight()) + + // 1. Identify the intended line index based on y + // layout.Y values are relative to the text region origin. + // We need to account for scroll offset. + visualLine := int((y + float64(TheState.ScrollOffset)) / lineHeight) + + // Group glyphs by their Y-baseline + type lineGroup struct { + y float64 + indices []int + } + groups := []lineGroup{} + seenY := make(map[float64]int) // maps Y to group index + + for i, yVal := range layout.Y { + yFloat := float64(yVal) + idx, ok := seenY[yFloat] + if !ok { + idx = len(groups) + groups = append(groups, lineGroup{y: yFloat, indices: []int{}}) + seenY[yFloat] = idx } + groups[idx].indices = append(groups[idx].indices, i) + } + // Sort groups by Y + sort.Slice(groups, func(i, j int) bool { return groups[i].y < groups[j].y }) + + // If visualLine is out of bounds, clamp + if visualLine < 0 { + visualLine = 0 + } + if visualLine >= len(groups) { + visualLine = len(groups) - 1 } - // 2. Identify glyphs on this line and find the rightmost extent + targetGroup := groups[visualLine] + + // 3. Identify rightmost extent on this line rightmostX := 0.0 rightmostIdx := -1 - for i, yVal := range layout.Y { - if yVal == bestLineY { - xEnd := float64(layout.X[i] + layout.Advance[i]) - if xEnd > rightmostX { - rightmostX = xEnd - rightmostIdx = i - } + for _, i := range targetGroup.indices { + xEnd := float64(layout.X[i] + layout.Advance[i]) + if xEnd > rightmostX { + rightmostX = xEnd + rightmostIdx = i } } - // 3. Check if tap is to the right of the last character on this line + // 4. Check if tap is to the right of the last character if rightmostIdx != -1 && x > rightmostX { - // Position after the last character on this line + // Position at the end of the line content, before any trailing newline. start := layout.ByteOffsets[rightmostIdx] - _, size := utf8.DecodeRuneInString(TheState.Editor.Buffer[start:]) - TheState.Editor.CursorPosition = start + size + r, size := utf8.DecodeRuneInString(TheState.Editor.Buffer[start:]) + if r == '\n' { + TheState.Editor.CursorPosition = start + } else { + TheState.Editor.CursorPosition = start + size + } return } - // 4. Otherwise, find the closest glyph on this line. + // 5. Otherwise, find the closest glyph on this line. bestIdx := -1 minDist := float64(1e9) - for i, yVal := range layout.Y { - if yVal == bestLineY { - if bestIdx == -1 { - bestIdx = i - } - // Calculate distance to the glyph center - glyphCenterX := float64(layout.X[i] + layout.Advance[i]/2) - dist := glyphCenterX - x - if dist < 0 { - dist = -dist - } - if dist < minDist { - minDist = dist - bestIdx = i - } + for _, i := range targetGroup.indices { + if bestIdx == -1 { + bestIdx = i + } + // Calculate distance to the glyph center + glyphCenterX := float64(layout.X[i] + layout.Advance[i]/2) + dist := glyphCenterX - x + if dist < 0 { + dist = -dist + } + if dist < minDist { + minDist = dist + bestIdx = i } } if bestIdx != -1 {