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>
This commit is contained in:
Chris Waldon
2022-10-17 07:59:24 -04:00
committed by Elias Naur
parent 2340664570
commit f7c14e9964
2 changed files with 65 additions and 40 deletions
+62 -37
View File
@@ -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) {
var eol bool
pos, eol, eof = incrementLinePosition(lines, alignment, width, pos)
if eof || eol {
return pos, eof
}
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
}
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.
+3 -3
View File
@@ -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
}
})