diff --git a/.gitignore b/.gitignore index bbe249f..8bae7d0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ # build files *.apk *.idsig +cmd/pad/pad # IDE .idea/ diff --git a/internal/editor/cursor_test.go b/internal/editor/cursor_test.go index 557e2f1..92352a4 100644 --- a/internal/editor/cursor_test.go +++ b/internal/editor/cursor_test.go @@ -2,22 +2,90 @@ package editor import ( "testing" + + "pad/internal/ui" ) -// TestByteOffsetToLineCol_Logical verifies that the current logic -// only handles logical lines (newline-delimited). -func TestByteOffsetToLineCol_Logical(t *testing.T) { - buf := "hello\nworld" - // 012345 67890 - - // Logical line 0: "hello" (0-4), pos 5 is '\n', line 1 starts at 6 - line, col := byteOffsetToLineCol(buf, 5) - if line != 0 || col != 5 { - t.Errorf("expected (0, 5) for pos 5, got (%d, %d)", line, col) +// TestCursorPositioning_Basic verifies cursor positioning via GlyphLayout. +func TestCursorPositioning_Basic(t *testing.T) { +TheState = NewState() +TheState.Editor.Buffer = "hello\nworld" + + // Two lines: "hello\n" (bytes 0-5) and "world" (bytes 6-10) + // Line 0 glyphs at Y=70, Line 1 glyphs at Y=140 + TheState.Editor.GlyphLayout = ui.GlyphLayout{ + ByteOffsets: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + X: []ui.Dp{10, 20, 30, 40, 50, 10, 20, 30, 40, 50, 60}, + Y: []ui.Dp{70, 70, 70, 70, 70, 70, 140, 140, 140, 140, 140}, + Advance: []ui.Dp{10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, } - line, col = byteOffsetToLineCol(buf, 6) - if line != 1 || col != 0 { - t.Errorf("expected (1, 0) for pos 6, got (%d, %d)", line, col) + // Test tapping on first line near X=30 (glyph index 2, byteOffset 2) + SetCursorFromPoint(25, 70) + if TheState.Editor.CursorPosition < 0 || TheState.Editor.CursorPosition > len(TheState.Editor.Buffer) { + t.Errorf("cursor position %d out of bounds after first tap", TheState.Editor.CursorPosition) + } + + // Test tapping on second line + SetCursorFromPoint(35, 140) + if TheState.Editor.CursorPosition < 0 || TheState.Editor.CursorPosition > len(TheState.Editor.Buffer) { + t.Errorf("cursor position %d out of bounds after second tap", TheState.Editor.CursorPosition) + } +} + +// TestCursorPositioning_EmptyDocument verifies cursor positioning handles empty documents. +func TestCursorPositioning_EmptyDocument(t *testing.T) { +TheState = NewState() +TheState.Editor.Buffer = "" +TheState.Editor.GlyphLayout = ui.GlyphLayout{} + + // Should not panic on empty layout + SetCursorFromPoint(50, 50) + + if TheState.Editor.CursorPosition != 0 { + t.Errorf("expected cursor at 0 for empty document, got %d", TheState.Editor.CursorPosition) + } +} + +// TestCursorPositioning_IncompleteLayout verifies cursor positioning handles missing layout slices. +func TestCursorPositioning_IncompleteLayout(t *testing.T) { +TheState = NewState() +TheState.Editor.Buffer = "test" + // GlyphLayout with ByteOffsets and X but no Advance (partial data) + TheState.Editor.GlyphLayout = ui.GlyphLayout{ + ByteOffsets: []int{0, 1, 2, 3}, + X: []ui.Dp{10, 20, 30, 40}, + Y: []ui.Dp{70, 70, 70, 70}, + // Advance intentionally omitted + } + + // Should not panic when Advance is missing + SetCursorFromPoint(25, 70) +} + +// TestHandleCursorMove_Bounds verifies cursor movement stays within buffer bounds. +func TestHandleCursorMove_Bounds(t *testing.T) { +TheState = NewState() +TheState.Editor.Buffer = "hello" +TheState.Editor.CursorPosition = 0 + + // Move right 3 times + HandleCursorMove(1) + HandleCursorMove(1) + HandleCursorMove(1) + if TheState.Editor.CursorPosition != 3 { + t.Errorf("expected cursor at 3, got %d", TheState.Editor.CursorPosition) + } + + // Move right past end — should clamp to buffer length + HandleCursorMove(10) + if TheState.Editor.CursorPosition != len(TheState.Editor.Buffer) { + t.Errorf("expected cursor at %d (end of buffer), got %d", len(TheState.Editor.Buffer), TheState.Editor.CursorPosition) + } + + // Move left past start — should clamp to 0 + HandleCursorMove(-100) + if TheState.Editor.CursorPosition != 0 { + t.Errorf("expected cursor at 0, got %d", TheState.Editor.CursorPosition) } } diff --git a/internal/editor/state.go b/internal/editor/state.go index 1b6a8f7..d2eaacc 100644 --- a/internal/editor/state.go +++ b/internal/editor/state.go @@ -453,7 +453,7 @@ func EditorLayout(screenWidth, screenHeight ui.Dp, wordWrap bool) []ui.Element { // SetCursorFromPoint updates the cursor position based on screen coordinates (Dp). func SetCursorFromPoint(x, y float64) { layout := TheState.Editor.GlyphLayout - if len(layout.ByteOffsets) == 0 { + if len(layout.ByteOffsets) == 0 || len(layout.X) == 0 || len(layout.Advance) == 0 { return } diff --git a/internal/test/e2e/cursor_interaction_test.go b/internal/test/e2e/cursor_interaction_test.go index 5aa447f..61b15dd 100644 --- a/internal/test/e2e/cursor_interaction_test.go +++ b/internal/test/e2e/cursor_interaction_test.go @@ -29,6 +29,7 @@ func TestEditorClickToMoveCursor(t *testing.T) { ByteOffsets: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, X: []ui.Dp{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120}, Y: []ui.Dp{70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70}, + Advance: []ui.Dp{10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, } // Initial cursor position should be 0 (or end of text, depending on implementation)