editor: add window base offset to tap/cursor positioning
The GlyphLayout is shaped from the visible window alone, so its ByteOffsets are relative to the window start (IMEWindowStartByte). But SetCursorFromPoint, HandleHome, HandleEnd and HandleVerticalCursorMove treated them as absolute file offsets. On a large file scrolled deep, the cursor was set to a window-relative offset (near the file start), so the next EditorLayout computed visibleCursorPos = CursorPosition - start as a small/negative value and the IME selection snapped to the window top (regardless of where the user tapped). Add glyphBase() and convert at the four cursor boundaries: subtract the base before searching the window-relative offsets, add it back before storing the absolute CursorPosition. For small files (window == whole file) the base is 0 and the change is a no-op. Add TestTapToPosition_WindowBaseOffset, a regression test that sets a non-zero IMEWindowStartByte and asserts the cursor lands at base + window byte (fails on the pre-fix window-relative assignment). Verified on-device: with a 6 MB file scrolled to ~line 1200, a tap now places the cursor on the tapped line (typed text lands ~16 lines below the window top), where before it always landed on the window top.
This commit is contained in:
parent
e11d705196
commit
3bc3ed3530
|
|
@ -111,6 +111,49 @@ func TestTapLocalY_WindowRelative(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestTapToPosition_WindowBaseOffset is a regression test for the
|
||||
// window-relative GlyphLayout bug: the GlyphLayout is shaped from the visible
|
||||
// window alone, so its ByteOffsets are relative to the window start
|
||||
// (IMEWindowStartByte). SetCursorFromPoint must add that base to produce an
|
||||
// absolute file offset. Before the fix, a tap on a scrolled large file set the
|
||||
// cursor to a window-relative offset (near the file start), so the IME
|
||||
// selection snapped to the window top on every tap.
|
||||
func TestTapToPosition_WindowBaseOffset(t *testing.T) {
|
||||
lh := float64(EditorLineHeight())
|
||||
const lines = 40
|
||||
gl := ui.GlyphLayout{LineHeight: EditorLineHeight()}
|
||||
for i := 0; i < lines; i++ {
|
||||
for _, x := range []ui.Dp{10, 20} {
|
||||
gl.ByteOffsets = append(gl.ByteOffsets, i*2) // window-relative
|
||||
gl.X = append(gl.X, x)
|
||||
gl.Y = append(gl.Y, ui.Dp(float64(i+1)*lh))
|
||||
gl.Advance = append(gl.Advance, 10)
|
||||
}
|
||||
}
|
||||
TheState = NewState()
|
||||
TheState.Editor.GlyphLayout = gl
|
||||
TheState.Editor.IMEWindowStartByte = 47618 // window starts deep in the file
|
||||
TheState.ScrollOffset = 16500
|
||||
|
||||
// Tap ~3 lines below the top of the window.
|
||||
regionTop := ui.Dp(114)
|
||||
ptY := regionTop + ui.Dp(3*lh)
|
||||
localY := tapLocalY(ptY, regionTop, TheState.ScrollOffset)
|
||||
SetCursorFromPoint(15, localY)
|
||||
|
||||
// Window line 3 is window-relative byte 6, so the absolute cursor must be
|
||||
// base (47618) + 6 = 47624. Before the fix it would be 6 (window-relative),
|
||||
// which is the file start, not the tapped line.
|
||||
want := 47618 + 6
|
||||
if TheState.Editor.CursorPosition < want-2 || TheState.Editor.CursorPosition > want+2 {
|
||||
t.Errorf("cursor = %d, want %d (base 47618 + window byte 6); window-relative offset bug?", TheState.Editor.CursorPosition, want)
|
||||
}
|
||||
// Regression: the buggy cursor would be ~6 (window-relative, near file start).
|
||||
if TheState.Editor.CursorPosition < 100 {
|
||||
t.Errorf("cursor = %d, window-relative offset leaked (should be absolute ~%d)", TheState.Editor.CursorPosition, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTapToPosition_LargeFileScrolled is the end-to-end regression: with a
|
||||
// window-relative GlyphLayout and a deep scroll, a tap a few lines below the top
|
||||
// of the window must land on that window line, NOT be clamped to the last window
|
||||
|
|
|
|||
|
|
@ -341,7 +341,6 @@ func HandleCursorMove(delta int) {
|
|||
if newPos > maxPos {
|
||||
newPos = maxPos
|
||||
}
|
||||
log.Printf("HandleCursorMove: old=%d, new=%d (max=%d)", TheState.Editor.CursorPosition, newPos, maxPos)
|
||||
TheState.Editor.CursorPosition = newPos
|
||||
}
|
||||
|
||||
|
|
@ -352,11 +351,9 @@ func HandleKeyDown(data any) {
|
|||
if TheState.Editor.TooLarge {
|
||||
return
|
||||
}
|
||||
log.Printf("HandleKeyDown: received data of type %T: %v", data, data)
|
||||
switch v := data.(type) {
|
||||
case key.EditEvent:
|
||||
// Text input from IME / keyboard.
|
||||
log.Printf("HandleKeyDown: EditEvent text=%q range=%+v", v.Text, v.Range)
|
||||
if v.Text == "\b" {
|
||||
// Backspace character (legacy / hardware): delete one char before
|
||||
// the cursor.
|
||||
|
|
@ -370,7 +367,6 @@ func HandleKeyDown(data any) {
|
|||
HandleReplaceRange(v.Range.Start, v.Range.End, v.Text)
|
||||
}
|
||||
case key.Name:
|
||||
log.Printf("HandleKeyDown: name=%q", v)
|
||||
switch v {
|
||||
case key.NameLeftArrow:
|
||||
HandleCursorMove(-1)
|
||||
|
|
@ -398,6 +394,15 @@ func HandleKeyDown(data any) {
|
|||
}
|
||||
}
|
||||
|
||||
// glyphBase returns the absolute file byte offset at the start of the current
|
||||
// visible window. The GlyphLayout is shaped from the visible window alone, so
|
||||
// every one of its ByteOffsets is relative to this base; adding it yields an
|
||||
// absolute file offset. For a whole-file window (small files, or scroll 0) the
|
||||
// base is 0 and this is a no-op.
|
||||
func glyphBase() int {
|
||||
return TheState.Editor.IMEWindowStartByte
|
||||
}
|
||||
|
||||
// HandleHome moves the cursor to the start of the current visual line.
|
||||
func HandleHome() {
|
||||
layout := TheState.Editor.GlyphLayout
|
||||
|
|
@ -405,8 +410,12 @@ func HandleHome() {
|
|||
return
|
||||
}
|
||||
|
||||
pos := TheState.Editor.CursorPosition
|
||||
// Find current glyph index.
|
||||
base := glyphBase()
|
||||
pos := TheState.Editor.CursorPosition - base
|
||||
if pos < 0 {
|
||||
pos = 0
|
||||
}
|
||||
// Find current glyph index (offsets are window-relative).
|
||||
idx := sort.Search(len(layout.ByteOffsets), func(i int) bool {
|
||||
return layout.ByteOffsets[i] >= pos
|
||||
})
|
||||
|
|
@ -424,7 +433,7 @@ func HandleHome() {
|
|||
break
|
||||
}
|
||||
}
|
||||
TheState.Editor.CursorPosition = layout.ByteOffsets[targetIdx]
|
||||
TheState.Editor.CursorPosition = base + layout.ByteOffsets[targetIdx]
|
||||
}
|
||||
|
||||
// HandleEnd moves the cursor to the end of the current visual line.
|
||||
|
|
@ -434,8 +443,12 @@ func HandleEnd() {
|
|||
return
|
||||
}
|
||||
|
||||
pos := TheState.Editor.CursorPosition
|
||||
// Find current glyph index.
|
||||
base := glyphBase()
|
||||
pos := TheState.Editor.CursorPosition - base
|
||||
if pos < 0 {
|
||||
pos = 0
|
||||
}
|
||||
// Find current glyph index (offsets are window-relative).
|
||||
idx := sort.Search(len(layout.ByteOffsets), func(i int) bool {
|
||||
return layout.ByteOffsets[i] >= pos
|
||||
})
|
||||
|
|
@ -456,7 +469,8 @@ func HandleEnd() {
|
|||
|
||||
// Position after the last character of the line.
|
||||
// If it's a newline, it's the newline itself.
|
||||
start := layout.ByteOffsets[targetIdx]
|
||||
// start is window-relative; convert to an absolute file offset.
|
||||
start := base + layout.ByteOffsets[targetIdx]
|
||||
buf := TheState.Editor.ChunkedBuffer
|
||||
var fileContent string
|
||||
if buf != nil {
|
||||
|
|
@ -503,8 +517,12 @@ func HandleVerticalCursorMove(up bool) {
|
|||
return
|
||||
}
|
||||
|
||||
pos := TheState.Editor.CursorPosition
|
||||
// Find current glyph index.
|
||||
base := glyphBase()
|
||||
pos := TheState.Editor.CursorPosition - base
|
||||
if pos < 0 {
|
||||
pos = 0
|
||||
}
|
||||
// Find current glyph index (offsets are window-relative).
|
||||
idx := sort.Search(len(layout.ByteOffsets), func(i int) bool {
|
||||
return layout.ByteOffsets[i] >= pos
|
||||
})
|
||||
|
|
@ -578,7 +596,7 @@ func HandleVerticalCursorMove(up bool) {
|
|||
}
|
||||
}
|
||||
|
||||
TheState.Editor.CursorPosition = layout.ByteOffsets[targetIdx]
|
||||
TheState.Editor.CursorPosition = base + layout.ByteOffsets[targetIdx]
|
||||
}
|
||||
|
||||
// HandleDelete removes the character after the cursor.
|
||||
|
|
@ -617,7 +635,6 @@ func HandleInsert(s string) {
|
|||
|
||||
// HandleBackspace removes the character before the cursor.
|
||||
func HandleBackspace() {
|
||||
log.Printf("HandleBackspace: CursorPosition=%d", TheState.Editor.CursorPosition)
|
||||
pos := TheState.Editor.CursorPosition
|
||||
if pos == 0 {
|
||||
return
|
||||
|
|
@ -749,9 +766,6 @@ func markDirty() {
|
|||
|
||||
// EditorLayout computes the element tree for the editor page.
|
||||
func EditorLayout(screenWidth, screenHeight ui.Dp, wordWrap bool) []ui.Element {
|
||||
// Debug: ensure we are actually running this
|
||||
// fmt.Printf("DEBUG: EditorLayout called\n")
|
||||
|
||||
margin := ui.Dp(10)
|
||||
|
||||
// --- Top bar: filename on row 1, icons on row 2 ---
|
||||
|
|
@ -1007,6 +1021,7 @@ func SetCursorFromPoint(x, y float64) {
|
|||
return
|
||||
}
|
||||
|
||||
base := glyphBase()
|
||||
lineHeight := float64(EditorLineHeight())
|
||||
|
||||
// 1. Identify the intended line index based on y
|
||||
|
|
@ -1091,7 +1106,7 @@ func SetCursorFromPoint(x, y float64) {
|
|||
// 4. Check if tap is to the right of the last character
|
||||
if rightmostIdx != -1 && x > rightmostX {
|
||||
// Position at the end of the line content, before any trailing newline.
|
||||
start := layout.ByteOffsets[rightmostIdx]
|
||||
start := base + layout.ByteOffsets[rightmostIdx]
|
||||
buf := TheState.Editor.ChunkedBuffer
|
||||
var fileContent string
|
||||
if buf != nil {
|
||||
|
|
@ -1132,6 +1147,6 @@ func SetCursorFromPoint(x, y float64) {
|
|||
}
|
||||
}
|
||||
if bestIdx != -1 {
|
||||
TheState.Editor.CursorPosition = layout.ByteOffsets[bestIdx]
|
||||
TheState.Editor.CursorPosition = base + layout.ByteOffsets[bestIdx]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user