diff --git a/widget/editor_test.go b/widget/editor_test.go index cc8d7aec..65deb613 100644 --- a/widget/editor_test.go +++ b/widget/editor_test.go @@ -1042,6 +1042,59 @@ func TestEditorSelectShortcuts(t *testing.T) { } } +func TestEditorSelect(t *testing.T) { + tFont := font.Font{} + tFontSize := unit.Sp(10) + tShaper := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection())) + tEditor := &Editor{ + SingleLine: false, + ReadOnly: true, + } + lines := "abc abc abc def def def ghi ghi ghi" + tEditor.SetText(lines) + + tRouter := new(input.Router) + gtx := layout.Context{ + Ops: new(op.Ops), + Locale: english, + Constraints: layout.Exact(image.Pt(50, 100)), + Source: tRouter.Source(), + } + gtx.Execute(key.FocusCmd{Tag: tEditor}) + tEditor.Layout(gtx, tShaper, tFont, tFontSize, op.CallOp{}, op.CallOp{}) + + index := tEditor.text.index + + firstLineGlyphs := index.lines[0].glyphs + firstLine := lines[:firstLineGlyphs] + yOffFirstLineCenter := index.lines[0].yOff / 2 + + tEditor.ClearSelection() + tEditor.text.MoveCoord(image.Pt(100, yOffFirstLineCenter)) + if cStart, cEnd := tEditor.Selection(); cStart != len(firstLine) || cEnd != 0 { + t.Errorf("TestEditorSelect %d: initial selection", len(firstLine)) + } + if got := tEditor.SelectedText(); got != firstLine { + t.Errorf("TestEditorSelect : Expected %q, got %q", firstLine, got) + } + + yOffSecondLineCenter := (index.lines[1].yOff-index.lines[0].yOff)/2 + index.lines[0].yOff + tEditor.text.MoveCoord(image.Pt(100, yOffSecondLineCenter)) + + secondLineGlyphs := index.lines[1].glyphs + firstTwoLines := lines[:firstLineGlyphs+secondLineGlyphs] + if got := tEditor.SelectedText(); got != firstTwoLines { + t.Errorf("TestEditorSelect : Expected %q, got %q", firstTwoLines, got) + } + + tEditor.Layout(gtx, tShaper, tFont, tFontSize, op.CallOp{}, op.CallOp{}) + firstLineEnd := index.lines[0].width.Round() + firstRegionMaxWidth := tEditor.text.regions[0].Bounds.Max.X + if firstRegionMaxWidth != firstLineEnd { + t.Errorf("Selection paint should contain last character") + } +} + // Verify that an existing selection is dismissed when you press arrow keys. func TestSelectMove(t *testing.T) { e := new(Editor) diff --git a/widget/index.go b/widget/index.go index 3d8fb3d2..8db6c0e9 100644 --- a/widget/index.go +++ b/widget/index.go @@ -22,6 +22,10 @@ type lineInfo struct { glyphs int } +func (l lineInfo) getLineEnd() fixed.Int26_6 { + return l.xOff + l.width +} + type glyphIndex struct { // glyphs holds the glyphs processed. glyphs []text.Glyph @@ -231,46 +235,61 @@ func (g *glyphIndex) Glyph(gl text.Glyph) { } func (g *glyphIndex) closestToRune(runeIdx int) (combinedPos, int) { - if len(g.positions) == 0 { + n := len(g.positions) + if n == 0 { return combinedPos{}, 0 } - i := sort.Search(len(g.positions), func(i int) bool { + i := sort.Search(n, func(i int) bool { pos := g.positions[i] return pos.runes >= runeIdx }) - if i > 0 { - i-- + + notFound := i == n + if notFound { + return g.positions[n-1], n - 1 } - closest := g.positions[i] - closestI := i - for ; i < len(g.positions); i++ { - if g.positions[i].runes == runeIdx { - return g.positions[i], i - } - } - return closest, closestI + return g.positions[i], i } func (g *glyphIndex) closestToLineCol(lineCol screenPos) combinedPos { - if len(g.positions) == 0 { + n := len(g.positions) + if n == 0 { return combinedPos{} } - i := sort.Search(len(g.positions), func(i int) bool { + i := sort.Search(n, func(i int) bool { pos := g.positions[i] return pos.lineCol.line > lineCol.line || (pos.lineCol.line == lineCol.line && pos.lineCol.col >= lineCol.col) }) - if i > 0 { - i-- + notFound := i == n + if notFound { + return g.positions[n-1] } - prior := g.positions[i] - if i+1 >= len(g.positions) { + pos := g.positions[i] + foundInNextLine := pos.lineCol.line > lineCol.line + if foundInNextLine && i > 0 { + prior := g.positions[i-1] + prior.x = g.lines[lineCol.line].getLineEnd() return prior } - next := g.positions[i+1] - if next.lineCol != lineCol { - return prior + return pos +} + +func (g *glyphIndex) atStartOfLine(pos combinedPos) bool { + if pos.runes == 0 { + return true } - return next + prevRuneIndex := pos.runes - 1 + lineOfPrevRune := g.positions[prevRuneIndex].lineCol.line + return lineOfPrevRune < pos.lineCol.line +} + +func (g *glyphIndex) atEndOfLine(pos combinedPos) bool { + if pos.runes == g.positions[len(g.positions)-1].runes { + return true + } + next := pos.runes + 1 + hasNext := next < len(g.positions) + return hasNext && g.positions[next].lineCol.line > pos.lineCol.line } func dist(a, b fixed.Int26_6) fixed.Int26_6 { @@ -280,9 +299,9 @@ func dist(a, b fixed.Int26_6) fixed.Int26_6 { return b - a } -func (g *glyphIndex) closestToXY(x fixed.Int26_6, y int) combinedPos { +func (g *glyphIndex) closestToXY(x fixed.Int26_6, y int) (pos combinedPos, atEndOfLine bool) { if len(g.positions) == 0 { - return combinedPos{} + return combinedPos{}, false } i := sort.Search(len(g.positions), func(i int) bool { pos := g.positions[i] @@ -292,7 +311,7 @@ func (g *glyphIndex) closestToXY(x fixed.Int26_6, y int) combinedPos { // short. Return either the last position or (if there are no // positions) the zero position. if i == len(g.positions) { - return g.positions[i-1] + return g.positions[i-1], false } first := g.positions[i] // Find the best X coordinate. @@ -308,14 +327,22 @@ func (g *glyphIndex) closestToXY(x fixed.Int26_6, y int) combinedPos { distance := dist(candidate.x, x) // If we are *really* close to the current position candidate, just choose it. if distance.Round() == 0 { - return g.positions[i] + return g.positions[i], false } if distance < closestDist { closestDist = distance closest = i } } - return g.positions[closest] + next := closest + 1 + hasNext := next < len(g.positions) + if hasNext && g.atEndOfLine(g.positions[closest]) { + distance := dist(g.lines[line].getLineEnd(), x) + if distance < closestDist { + return g.positions[next], true + } + } + return g.positions[closest], false } // makeRegion creates a text-aligned rectangle from start to end. The vertical diff --git a/widget/text.go b/widget/text.go index 8ddc5b28..eaf7ecae 100644 --- a/widget/text.go +++ b/widget/text.go @@ -173,14 +173,17 @@ func (e *textView) closestToLineCol(line, col int) combinedPos { return e.index.closestToLineCol(screenPos{line: line, col: col}) } -func (e *textView) closestToXY(x fixed.Int26_6, y int) combinedPos { +func (e *textView) closestToXY(x fixed.Int26_6, y int) (combinedPos, bool) { e.makeValid() return e.index.closestToXY(x, y) } -func (e *textView) closestToXYGraphemes(x fixed.Int26_6, y int) combinedPos { +func (e *textView) closestToXYGraphemes(x fixed.Int26_6, y int) (combinedPos, bool) { // Find the closest existing rune position to the provided coordinates. - pos := e.closestToXY(x, y) + pos, atEndOfLine := e.closestToXY(x, y) + if atEndOfLine { + return pos, true + } // Resolve cluster boundaries on either side of the rune position. firstOption := e.moveByGraphemes(pos.runes, 0) distance := 1 @@ -194,9 +197,9 @@ func (e *textView) closestToXYGraphemes(x fixed.Int26_6, y int) combinedPos { second := e.closestToRune(secondOption) secondDist := absFixed(second.x - x) if firstDist > secondDist { - return second + return second, false } else { - return first + return first, false } } @@ -214,8 +217,11 @@ func (e *textView) MoveLines(distance int, selAct selectionAction) { x := caretStart.x + e.caret.xoff // Seek to line. pos := e.closestToLineCol(caretStart.lineCol.line+distance, 0) - pos = e.closestToXYGraphemes(x, pos.y) + pos, atEndOfLine := e.closestToXYGraphemes(x, pos.y) e.caret.start = pos.runes + if atEndOfLine && pos.runes > 0 { + e.caret.start = pos.runes - 1 + } e.caret.xoff = x - pos.x e.updateSelection(selAct) } @@ -472,7 +478,8 @@ func (e *textView) scrollAbs(x, y int) { func (e *textView) MoveCoord(pos image.Point) { x := fixed.I(pos.X + e.scrollOff.X) y := pos.Y + e.scrollOff.Y - e.caret.start = e.closestToXYGraphemes(x, y).runes + p, _ := e.closestToXYGraphemes(x, y) + e.caret.start = p.runes e.caret.xoff = 0 } @@ -610,7 +617,7 @@ func (e *textView) MovePages(pages int, selAct selectionAction) { caret := e.closestToRune(e.caret.start) x := caret.x + e.caret.xoff y := caret.y + pages*e.viewSize.Y - pos := e.closestToXYGraphemes(x, y) + pos, _ := e.closestToXYGraphemes(x, y) e.caret.start = pos.runes e.caret.xoff = x - pos.x e.updateSelection(selAct)