~eliasnaur/gio

64db3720fcf3bc274efdc99df4b55f75520fb540 — Chris Waldon 11 months ago b67b322
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 <christopher.waldon.dev@gmail.com>
2 files changed, 121 insertions(+), 1 deletions(-)

M widget/editor.go
M widget/text_test.go
M widget/editor.go => widget/editor.go +10 -1
@@ 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)

M widget/text_test.go => widget/text_test.go +111 -0
@@ 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)
				}
			})
		}
	}
}