~eliasnaur/gio

dee3cc44f9da554b81a8ddd93ac01763ac3427c2 — Chris Waldon 11 months ago 64db372
widget: test positionGreaterOrEqual

This commit adds an exhaustive test case for the positionGreaterOrEqual
helper function that our text widgets use to compare locations within
shaped text.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2 files changed, 93 insertions(+), 7 deletions(-)

M widget/editor.go
M widget/text_test.go
M widget/editor.go => widget/editor.go +2 -1
@@ 1000,7 1000,8 @@ func (e *Editor) indexPosition(pos combinedPos) combinedPos {
}

// positionGreaterOrEqual reports whether p1 >= p2 according to the non-zero fields
// of p2. All fields of p1 must be a consistent and valid.
// of p2. All fields of p1 must be a consistent and valid. The clusterIndex field
// is never considered, as it is a line-local property.
func positionGreaterOrEqual(lines []text.Line, p1, p2 combinedPos) bool {
	l := lines[p1.lineCol.Y]
	endCol := l.Layout.Runes.Count - 1

M widget/text_test.go => widget/text_test.go +91 -6
@@ 12,7 12,10 @@ import (
	"golang.org/x/image/math/fixed"
)

func makeTestText(fontSize, lineWidth int) ([]text.Line, []text.Line) {
// makeTestText returns an ltr and rtl sample of shaped text at the given
// font size and wrapped to the given line width. The runeLimit, if nonzero,
// truncates the sample text to ensure shorter output for expensive tests.
func makeTestText(fontSize, lineWidth, runeLimit int) ([]text.Line, []text.Line) {
	ltrFace, _ := opentype.Parse(goregular.TTF)
	rtlFace, _ := opentype.Parse(nsareg.TTF)



@@ 26,15 29,27 @@ func makeTestText(fontSize, lineWidth int) ([]text.Line, []text.Line) {
			Face: rtlFace,
		},
	})
	ltrText := shaper.LayoutString(text.Font{Typeface: "LTR"}, fixed.I(fontSize), lineWidth, english, "The quick brown fox\njumps over the lazy dog.")
	rtlText := shaper.LayoutString(text.Font{Typeface: "RTL"}, fixed.I(fontSize), lineWidth, arabic, "الحب سماء لا\nتمط غير الأحلام")
	ltrSource := "The quick brown fox\njumps over the lazy dog."
	rtlSource := "الحب سماء لا\nتمط غير الأحلام"
	if runeLimit != 0 {
		ltrRunes := []rune(ltrSource)
		rtlRunes := []rune(rtlSource)
		if runeLimit < len(ltrRunes) {
			ltrSource = string(ltrRunes[:runeLimit])
		}
		if runeLimit < len(rtlRunes) {
			rtlSource = string(rtlRunes[:runeLimit])
		}
	}
	ltrText := shaper.LayoutString(text.Font{Typeface: "LTR"}, fixed.I(fontSize), lineWidth, english, ltrSource)
	rtlText := shaper.LayoutString(text.Font{Typeface: "RTL"}, fixed.I(fontSize), lineWidth, arabic, rtlSource)
	return ltrText, rtlText
}

func TestFirstPos(t *testing.T) {
	fontSize := 16
	lineWidth := fontSize * 10
	ltrText, rtlText := makeTestText(fontSize, lineWidth)
	ltrText, rtlText := makeTestText(fontSize, lineWidth, 0)
	type testcase struct {
		name      string
		line      text.Line


@@ 151,7 166,7 @@ func TestFirstPos(t *testing.T) {
func TestIncrementPosition(t *testing.T) {
	fontSize := 16
	lineWidth := fontSize * 3
	ltrText, rtlText := makeTestText(fontSize, lineWidth)
	ltrText, rtlText := makeTestText(fontSize, lineWidth, 0)
	type trial struct {
		input, output combinedPos
	}


@@ 248,7 263,7 @@ func TestIncrementPosition(t *testing.T) {
func TestClusterIndexFor(t *testing.T) {
	fontSize := 16
	lineWidth := fontSize * 3
	ltrText, rtlText := makeTestText(fontSize, lineWidth)
	ltrText, rtlText := makeTestText(fontSize, lineWidth, 0)
	type input struct {
		runeIdx         int
		clusterStartIdx int


@@ 355,3 370,73 @@ func TestClusterIndexFor(t *testing.T) {
		}
	}
}

func TestPositionGreaterOrEqual(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,
		},
	} {
		finalLineRunes := tc.lines[len(tc.lines)-1].Layout.Runes
		// Statically generate all valid positions.
		positions := make([]combinedPos, finalLineRunes.Offset+finalLineRunes.Count+1)
		positions[0] = firstPos(tc.lines[0], tc.align, tc.width)
		for i := 1; i < len(positions); i++ {
			positions[i], _ = incrementPosition(tc.lines, tc.align, tc.width, positions[i-1])
		}
		// For each valid position, check every other valid position returns the correct
		// result with each permutation of populated fields.
		for i, p1 := range positions {
			for k, p2 := range positions {
				t.Run(tc.name+" "+strconv.Itoa(i)+">="+strconv.Itoa(k), func(t *testing.T) {
					for kind := 0; kind < 3; kind++ {
						p2 := p2
						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{}
						}
						isGreaterOrEqual := i >= k
						result := positionGreaterOrEqual(tc.lines, p1, p2)
						if result != isGreaterOrEqual {
							t.Errorf("unexpected result comparing p[%d] >= p[%d](%s) (%v)\np1: %#+v\np2:%#+v", i, k, transform, result, p1, p2)
						}
					}
				})
			}
		}
	}
}