From 64db3720fcf3bc274efdc99df4b55f75520fb540 Mon Sep 17 00:00:00 2001 From: Chris Waldon Date: Thu, 13 Oct 2022 12:43:32 -0400 Subject: [PATCH] widget: test and document clusterIndexFor This commit adds documentation and tests for the clusterIndexFor helper, making it easier to understand what it does and how to use it safely. Signed-off-by: Chris Waldon --- widget/editor.go | 11 ++++- widget/text_test.go | 111 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/widget/editor.go b/widget/editor.go index 10181931..a631cc37 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -1049,7 +1049,16 @@ func positionGreaterOrEqual(lines []text.Line, p1, p2 combinedPos) bool { // at the given position within the line. As a special case, if the rune is one // beyond the final rune of the line, it returns the length of the line's clusters // slice. Otherwise, it panics if given a rune beyond the -// dimensions of the line. +// dimensions of the line. The startIdx must be known to be at or before +// the real index of the cluster. This means that this function is +// only useful for searching forward through text, not backward. +// Passing a startIdx after the cluster corresponding to the runeIdx +// will trigger a panic. +// +// All indices are relative to the content in the line. The runeIdx 0 +// refers to the first rune on the line, regardless of the line's +// rune offset. Similarly, the provided and returned glyph cluster +// indices are relative to the line's cluster slice. func clusterIndexFor(line text.Line, runeIdx, startIdx int) int { if runeIdx == line.Layout.Runes.Count { return len(line.Layout.Clusters) diff --git a/widget/text_test.go b/widget/text_test.go index f3e98d9e..34d28365 100644 --- a/widget/text_test.go +++ b/widget/text_test.go @@ -244,3 +244,114 @@ func TestIncrementPosition(t *testing.T) { }) } } + +func TestClusterIndexFor(t *testing.T) { + fontSize := 16 + lineWidth := fontSize * 3 + ltrText, rtlText := makeTestText(fontSize, lineWidth) + type input struct { + runeIdx int + clusterStartIdx int + expected int + panics bool + } + type testcase struct { + name string + line text.Line + inputs []input + } + for _, tc := range []testcase{ + { + name: "ltr", + line: ltrText[0], + inputs: []input{ + {runeIdx: 0, clusterStartIdx: 0, expected: 0}, + {runeIdx: 1, clusterStartIdx: 0, expected: 1}, + {runeIdx: 1, clusterStartIdx: 1, expected: 1}, + {runeIdx: 1, clusterStartIdx: 2, panics: true}, + {runeIdx: 2, clusterStartIdx: 0, expected: 2}, + {runeIdx: 2, clusterStartIdx: 1, expected: 2}, + {runeIdx: 2, clusterStartIdx: 2, expected: 2}, + {runeIdx: 3, clusterStartIdx: 0, expected: 3}, + {runeIdx: 3, clusterStartIdx: 1, expected: 3}, + {runeIdx: 3, clusterStartIdx: 2, expected: 3}, + {runeIdx: 3, clusterStartIdx: 3, expected: 3}, + {runeIdx: 4, clusterStartIdx: 0, expected: 4}, + {runeIdx: 4, clusterStartIdx: 1, expected: 4}, + {runeIdx: 4, clusterStartIdx: 2, expected: 4}, + {runeIdx: 4, clusterStartIdx: 3, expected: 4}, + {runeIdx: 4, clusterStartIdx: 4, expected: 4}, + {runeIdx: 5, panics: true}, + }, + }, + { + name: "rtl", + line: rtlText[0], + inputs: []input{ + {runeIdx: 0, clusterStartIdx: 0, expected: 0}, + {runeIdx: 1, clusterStartIdx: 0, expected: 1}, + {runeIdx: 1, clusterStartIdx: 1, expected: 1}, + {runeIdx: 1, clusterStartIdx: 2, panics: true}, + {runeIdx: 2, clusterStartIdx: 0, expected: 2}, + {runeIdx: 2, clusterStartIdx: 1, expected: 2}, + {runeIdx: 2, clusterStartIdx: 2, expected: 2}, + {runeIdx: 3, clusterStartIdx: 0, expected: 3}, + {runeIdx: 3, clusterStartIdx: 1, expected: 3}, + {runeIdx: 3, clusterStartIdx: 2, expected: 3}, + {runeIdx: 3, clusterStartIdx: 3, expected: 3}, + {runeIdx: 4, clusterStartIdx: 0, expected: 4}, + {runeIdx: 4, clusterStartIdx: 1, expected: 4}, + {runeIdx: 4, clusterStartIdx: 2, expected: 4}, + {runeIdx: 4, clusterStartIdx: 3, expected: 4}, + {runeIdx: 4, clusterStartIdx: 4, expected: 4}, + {runeIdx: 5, clusterStartIdx: 0, expected: 5}, + {runeIdx: 6, panics: true}, + }, + }, + { + name: "rtl-ligatures", + line: rtlText[4], + inputs: []input{ + {runeIdx: 0, clusterStartIdx: 0, expected: 0}, + {runeIdx: 1, clusterStartIdx: 0, expected: 1}, + {runeIdx: 1, clusterStartIdx: 1, expected: 1}, + {runeIdx: 2, clusterStartIdx: 0, expected: 1}, + {runeIdx: 2, clusterStartIdx: 1, expected: 1}, + {runeIdx: 2, clusterStartIdx: 2, panics: true}, + {runeIdx: 3, clusterStartIdx: 0, expected: 2}, + {runeIdx: 3, clusterStartIdx: 1, expected: 2}, + {runeIdx: 3, clusterStartIdx: 2, expected: 2}, + {runeIdx: 4, clusterStartIdx: 0, expected: 3}, + {runeIdx: 4, clusterStartIdx: 1, expected: 3}, + {runeIdx: 4, clusterStartIdx: 2, expected: 3}, + {runeIdx: 4, clusterStartIdx: 3, expected: 3}, + {runeIdx: 5, clusterStartIdx: 0, expected: 3}, + {runeIdx: 5, clusterStartIdx: 1, expected: 3}, + {runeIdx: 5, clusterStartIdx: 2, expected: 3}, + {runeIdx: 5, clusterStartIdx: 3, expected: 3}, + {runeIdx: 6, clusterStartIdx: 0, expected: 4}, + {runeIdx: 6, clusterStartIdx: 1, expected: 4}, + {runeIdx: 6, clusterStartIdx: 2, expected: 4}, + {runeIdx: 6, clusterStartIdx: 3, expected: 4}, + {runeIdx: 6, clusterStartIdx: 4, expected: 4}, + {runeIdx: 7, clusterStartIdx: 0, expected: 5}, + {runeIdx: 8, panics: true}, + }, + }, + } { + for i, input := range tc.inputs { + t.Run(tc.name+strconv.Itoa(i), func(t *testing.T) { + defer func() { + err := recover() + if err != nil != input.panics { + t.Errorf("panic state mismatch") + } + }() + actual := clusterIndexFor(tc.line, input.runeIdx, input.clusterStartIdx) + if actual != input.expected { + t.Errorf("input[%d]: expected %d, got %d", i, input.expected, actual) + } + }) + } + } +}