~eliasnaur/gio

f7c14e9964dd05611289aec6370b0eb5eb07b5cd — Chris Waldon 1 year, 10 months ago 2340664
widget: redefine >= and ++ on combinedPos

This commit redefines incrementing a combinedPos to either move a single
rune forward, *or* transition from EOL->BOL, *or* both. This allows traversal
of lines without a trailing newline character to reach the position after the
final glyph of content.

Additionally, this commit updates positionGreaterOrEqual to explicitly handle
hard newlines via special-case logic, allowing lines without a hard newline to
avoid the newline-based short-circuit logic that would prevent them from iteratively
reaching the combinedPos following the final glyph on the line.

Fixes: https://todo.sr.ht/~eliasnaur/gio/400

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

M widget/editor.go
M widget/text_test.go
M widget/editor.go => widget/editor.go +62 -37
@@ 1000,13 1000,17 @@ 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. The clusterIndex field
// is never considered, as it is a line-local property.
// of p2. All fields of p1 must be consistent and valid.
func positionGreaterOrEqual(lines []text.Line, p1, p2 combinedPos) bool {
	l := lines[p1.lineCol.Y]
	endCol := l.Layout.Runes.Count - 1
	if lastLine := p1.lineCol.Y == len(lines)-1; lastLine {
		endCol++
	// Check whether the final glyph cluster has no glyphs, indicating a newline
	// rune that forced the existence of a line break.
	hardNewLine := len(l.Layout.Clusters) > 0 && l.Layout.Clusters[len(l.Layout.Clusters)-1].Glyphs.Count == 0
	endCol := l.Layout.Runes.Count
	if hardNewLine {
		// If there was a hard newline, prevent the cursor for passing it on
		// this line.
		endCol--
	}
	eol := p1.lineCol.X == endCol
	switch {


@@ 1031,17 1035,24 @@ func positionGreaterOrEqual(lines []text.Line, p1, p2 combinedPos) bool {
		if eol {
			return true
		}
		// If clusterIndex is equal, they could be positions of different
		// runes within the same cluster, so we fall back to the positional
		// test below.
		if p2.clusterIndex != 0 && p1.clusterIndex != p2.clusterIndex {
			return p1.clusterIndex > p2.clusterIndex
		}

		// Find the cluster containing the rune position described by p1
		// in order to determine the width of a rune within it.
		clusterIdx := clusterIndexFor(l, p1.lineCol.X, p1.clusterIndex)
		flip := l.Layout.Direction.Progression() == system.TowardOrigin
		adv := l.Layout.Clusters[clusterIdx].RuneWidth()
		if p1.clusterIndex == len(l.Layout.Clusters) {
			return (!flip && p1.x >= p2.x) || (flip && p1.x <= p2.x)
		} else {
			adv := l.Layout.Clusters[p1.clusterIndex].RuneWidth()

		left := p1.x + adv - p2.x
		right := p2.x - p1.x
			left := p1.x + adv - p2.x
			right := p2.x - p1.x

		return (!flip && left >= right) || (flip && left <= right)
			return (!flip && left >= right) || (flip && left <= right)
		}
	}
	return true
}


@@ 1114,39 1125,53 @@ func seekPosition(lines []text.Line, alignment text.Alignment, width int, start,
	}
}

// incrementPosition updates pos to be one rune further into the text.
// incrementLinePosition transitions pos from the end of a line to the beginning of the next one.
// If pos is not at the end of a line, it will have no effect. It returns the (possibly modified)
// position, whether it handled the transition from the end of a line, and whether the position
// is at the end of the text data.
func incrementLinePosition(lines []text.Line, alignment text.Alignment, width int, pos combinedPos) (_ combinedPos, eol, eof bool) {
	l := lines[pos.lineCol.Y]
	if pos.lineCol.X >= l.Layout.Runes.Count {
		if pos.lineCol.Y == len(lines)-1 {
			// End of file.
			return pos, false, true
		}
		// Move to next line.
		prevDesc := l.Descent
		pos.lineCol.Y++
		pos.lineCol.X = 0
		pos.clusterIndex = 0
		l = lines[pos.lineCol.Y]
		// Use firstPos to get the correct x coordinate of the beginning of the line.
		alignedPos := firstPos(l, alignment, width)
		pos.x = alignedPos.x
		pos.y += (prevDesc + l.Ascent).Ceil()
		return pos, true, false
	}
	return pos, false, false
}

// incrementPosition updates pos to be one position further into the text. This will either
// move pos one rune further into the text, transition pos from the end of one line to the
// beginning of the next, or both.
// All fields of pos must be valid before calling incrementPosition. eof will be true when
// pos represents the final text position in the lines.
func incrementPosition(lines []text.Line, alignment text.Alignment, width int, pos combinedPos) (_ combinedPos, eof bool) {
	l := lines[pos.lineCol.Y]
	handleLineTransition := func() bool {
		if pos.lineCol.X >= l.Layout.Runes.Count {
			if pos.lineCol.Y == len(lines)-1 {
				// End of file.
				return true
			}
			// Move to next line.
			prevDesc := l.Descent
			pos.lineCol.Y++
			pos.lineCol.X = 0
			pos.clusterIndex = 0
			l = lines[pos.lineCol.Y]
			// Use firstPos to get the correct x coordinate of the beginning of the line.
			alignedPos := firstPos(l, alignment, width)
			pos.x = alignedPos.x
			pos.y += (prevDesc + l.Ascent).Ceil()
		}
		return false
	}
	if handleLineTransition() {
		return pos, true
	var eol bool
	pos, eol, eof = incrementLinePosition(lines, alignment, width, pos)
	if eof || eol {
		return pos, eof
	}
	l := lines[pos.lineCol.Y]
	isHardNewLine := l.Layout.Clusters[pos.clusterIndex].Glyphs.Count == 0
	pos.x += l.Layout.Clusters[pos.clusterIndex].RuneWidth()
	pos.runes++
	pos.lineCol.X++
	pos.clusterIndex = clusterIndexFor(l, pos.lineCol.X, pos.clusterIndex)

	return pos, handleLineTransition()
	if isHardNewLine {
		pos, _, eof = incrementLinePosition(lines, alignment, width, pos)
	}
	return pos, false
}

// indexRune returns the latest rune index and byte offset no later than r.

M widget/text_test.go => widget/text_test.go +3 -3
@@ 231,6 231,9 @@ func TestIncrementPosition(t *testing.T) {
					if input.clusterIndex > output.clusterIndex {
						t.Errorf("iteration %d advanced clusterIndex incorrectly: input %d output %d", i, input.clusterIndex, output.clusterIndex)
					}
					if output.runes != input.runes+1 {
						t.Errorf("iteration %d advanced runes incorrectly: input %d output %d", i, input.runes, output.runes)
					}
				} else {
					if input.y >= output.y {
						t.Errorf("iteration %d advanced the wrong way on y axis: input %v(%d) output %v(%d)", i, input.y, input.y, output.y, output.y)


@@ 252,9 255,6 @@ func TestIncrementPosition(t *testing.T) {
						t.Errorf("iteration %d should have zeroed lineCol.X, got: %d", i, output.lineCol.X)
					}
				}
				if output.runes != input.runes+1 {
					t.Errorf("iteration %d advanced runes incorrectly: input %d output %d", i, input.runes, output.runes)
				}
				input = output
			}
		})