diff --git a/widget/editor.go b/widget/editor.go index 8c410a0a..9a91c5ba 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -1093,6 +1093,8 @@ func (e *Editor) closestPosition(pos combinedPos) combinedPos { // seekPosition seeks to the position closest to needle, starting at start and returns true. // If limit is non-zero, seekPosition stops seeks after limit runes and returns false. +// Start must have all fields valid, and needle must have at least one of runes, lineCol, +// or x+y valid. Start must be known to be before needle. func seekPosition(lines []text.Line, alignment text.Alignment, width int, start, needle combinedPos, limit int) (combinedPos, bool) { count := 0 // Advance until start is greater than or equal to needle. diff --git a/widget/text_test.go b/widget/text_test.go index 04c3a16d..af86356f 100644 --- a/widget/text_test.go +++ b/widget/text_test.go @@ -1,6 +1,7 @@ package widget import ( + "math" "strconv" "testing" @@ -440,3 +441,88 @@ func TestPositionGreaterOrEqual(t *testing.T) { } } } + +func TestSeekPosition(t *testing.T) { + fontSize := 16 + lineWidth := fontSize * 10 + // Be careful tuning the runeLimit here. This test case's complexity + // is O(N^2) where N=runeLimit. It's easy to make this test take a stupid + // amount of time accidentally. + ltrText, rtlText := makeTestText(fontSize, lineWidth, 15) + type testcase struct { + name string + lines []text.Line + align text.Alignment + width int + } + for _, tc := range []testcase{ + { + name: "ltr", + lines: ltrText, + align: text.Start, + width: lineWidth, + }, + { + name: "rtl", + lines: rtlText, + align: text.Start, + width: lineWidth, + }, + } { + t.Run(tc.name, func(t *testing.T) { + finalLineRunes := tc.lines[len(tc.lines)-1].Layout.Runes + // Statically generate all valid positions. + positions := make([]combinedPos, 1, finalLineRunes.Offset+finalLineRunes.Count+1) + positions[0] = firstPos(tc.lines[0], tc.align, tc.width) + for i := 1; ; i++ { + pos, eof := incrementPosition(tc.lines, tc.align, tc.width, positions[i-1]) + positions = append(positions, pos) + if eof { + break + } + } + for i, start := range positions { + for k, needle := range positions { + if k < i { + continue + } + t.Run(tc.name+" "+strconv.Itoa(i)+"->"+strconv.Itoa(k), func(t *testing.T) { + for kind := 0; kind < 3; kind++ { + p2 := needle + transform := "" + switch kind { + case 0: // only runes populated + transform = "runes only" + p2.lineCol = screenPos{} + p2.x = 0 + p2.y = 0 + case 1: // only lineCol populated + transform = "lineCol only" + p2.runes = 0 + p2.x = 0 + p2.y = 0 + case 2: // only x and y populated + transform = "x,y only" + p2.runes = 0 + p2.lineCol = screenPos{} + } + for limit := 0; limit <= 10; limit += 5 { + result, found := seekPosition(tc.lines, tc.align, tc.width, start, p2, limit) + if (found && result != needle) || (!found && needle.runes-start.runes < limit) { + t.Errorf("unexpected result seeking p[%d] -> p[%d](%s) (limit %d, found %v) = %#+v\np1: %#+v\np2:%#+v", i, k, transform, limit, found, result, start, p2) + } + } + } + }) + } + } + result, found := seekPosition(tc.lines, tc.align, tc.width, positions[0], combinedPos{runes: math.MaxInt}, 0) + if !found { + t.Errorf("reported hit limit on max int") + } + if expected := positions[len(positions)-1]; result != expected { + t.Errorf("expected maximum int position to equal final position.\nexpected %#+v\nactual %#+v", expected, result) + } + }) + } +}