mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-05 17:35:36 +00:00
widget: [API] change Editor.SelectionLen, Selection, SetCaret, Len to operate in runes
This change uncovered and fixes a bug in nullLayout. This is an API change; the methods operated in bytes before. Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
+23
-76
@@ -214,11 +214,11 @@ func (e *Editor) processEvents(gtx layout.Context) {
|
|||||||
// Can't process events without a shaper.
|
// Can't process events without a shaper.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
oldStart, oldLen := min(e.caret.start.ofs, e.caret.end.ofs), e.SelectionLen()
|
oldStart, oldLen := min(e.caret.start.runes, e.caret.end.runes), e.SelectionLen()
|
||||||
e.processPointer(gtx)
|
e.processPointer(gtx)
|
||||||
e.processKey(gtx)
|
e.processKey(gtx)
|
||||||
// Queue a SelectEvent if the selection changed, including if it went away.
|
// Queue a SelectEvent if the selection changed, including if it went away.
|
||||||
if newStart, newLen := min(e.caret.start.ofs, e.caret.end.ofs), e.SelectionLen(); oldStart != newStart || oldLen != newLen {
|
if newStart, newLen := min(e.caret.start.runes, e.caret.end.runes), e.SelectionLen(); oldStart != newStart || oldLen != newLen {
|
||||||
e.events = append(e.events, SelectEvent{})
|
e.events = append(e.events, SelectEvent{})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -658,9 +658,11 @@ func (e *Editor) PaintCaret(gtx layout.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Len is the length of the editor contents.
|
// Len is the length of the editor contents, in runes.
|
||||||
func (e *Editor) Len() int {
|
func (e *Editor) Len() int {
|
||||||
return e.rr.len()
|
e.makeValid()
|
||||||
|
end := e.closestPosition(combinedPos{runes: math.MaxInt})
|
||||||
|
return end.runes
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text returns the contents of the editor.
|
// Text returns the contents of the editor.
|
||||||
@@ -763,49 +765,6 @@ func (e *Editor) CaretCoords() f32.Point {
|
|||||||
return f32.Pt(float32(e.caret.start.x)/64, float32(e.caret.start.y))
|
return f32.Pt(float32(e.caret.start.x)/64, float32(e.caret.start.y))
|
||||||
}
|
}
|
||||||
|
|
||||||
// offsetToScreenPos takes an offset into the editor text (e.g.
|
|
||||||
// e.caret.end.ofs) and returns a combinedPos that corresponds to its current
|
|
||||||
// screen position. Hint is either the zero-value position or the result of
|
|
||||||
// a previous call with a lower offset.
|
|
||||||
//
|
|
||||||
// This function is written this way to take advantage of previous work done
|
|
||||||
// for offsets after the first. Otherwise you have to start from the top each
|
|
||||||
// time.
|
|
||||||
func (e *Editor) offsetToScreenPos(hint combinedPos, offset int) combinedPos {
|
|
||||||
if hint == (combinedPos{}) {
|
|
||||||
l := e.lines[0]
|
|
||||||
hint = combinedPos{
|
|
||||||
x: align(e.Alignment, l.Width, e.viewSize.X),
|
|
||||||
y: l.Ascent.Ceil(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LOOP:
|
|
||||||
for {
|
|
||||||
l := e.lines[hint.lineCol.Y]
|
|
||||||
for ; hint.lineCol.X < len(l.Layout.Advances); hint.lineCol.X++ {
|
|
||||||
if hint.ofs >= offset {
|
|
||||||
break LOOP
|
|
||||||
}
|
|
||||||
|
|
||||||
hint.x += l.Layout.Advances[hint.lineCol.X]
|
|
||||||
_, s := e.rr.runeAt(hint.ofs)
|
|
||||||
hint.ofs += s
|
|
||||||
hint.runes++
|
|
||||||
}
|
|
||||||
if lastLine := hint.lineCol.Y == len(e.lines)-1; lastLine || hint.ofs > offset {
|
|
||||||
break LOOP
|
|
||||||
}
|
|
||||||
|
|
||||||
prevDesc := l.Descent
|
|
||||||
hint.lineCol.Y++
|
|
||||||
hint.lineCol.X = 0
|
|
||||||
l = e.lines[hint.lineCol.Y]
|
|
||||||
hint.x = align(e.Alignment, l.Width, e.viewSize.X)
|
|
||||||
hint.y += (prevDesc + l.Ascent).Ceil()
|
|
||||||
}
|
|
||||||
return hint
|
|
||||||
}
|
|
||||||
|
|
||||||
// indexPosition returns the latest position from the index no later than pos.
|
// indexPosition returns the latest position from the index no later than pos.
|
||||||
func (e *Editor) indexPosition(pos combinedPos) combinedPos {
|
func (e *Editor) indexPosition(pos combinedPos) combinedPos {
|
||||||
// Initialize index with first caret position.
|
// Initialize index with first caret position.
|
||||||
@@ -1083,7 +1042,7 @@ func (e *Editor) movePosToLine(pos combinedPos, x fixed.Int26_6, line int) combi
|
|||||||
// negative distances moves backward. Distances are in runes.
|
// negative distances moves backward. Distances are in runes.
|
||||||
func (e *Editor) MoveCaret(startDelta, endDelta int) {
|
func (e *Editor) MoveCaret(startDelta, endDelta int) {
|
||||||
e.makeValid()
|
e.makeValid()
|
||||||
keepSame := e.caret.start.ofs == e.caret.end.ofs && startDelta == endDelta
|
keepSame := e.caret.start.runes == e.caret.end.runes && startDelta == endDelta
|
||||||
e.caret.start = e.movePos(e.caret.start, startDelta)
|
e.caret.start = e.movePos(e.caret.start, startDelta)
|
||||||
e.caret.xoff = 0
|
e.caret.xoff = 0
|
||||||
// If they were in the same place, and we're moving them the same distance,
|
// If they were in the same place, and we're moving them the same distance,
|
||||||
@@ -1309,28 +1268,23 @@ func (e *Editor) NumLines() int {
|
|||||||
return len(e.lines)
|
return len(e.lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SelectionLen returns the length of the selection, in bytes; it is
|
// SelectionLen returns the length of the selection, in runes; it is
|
||||||
// equivalent to len(e.SelectedText()).
|
// equivalent to utf8.RuneCountInString(e.SelectedText()).
|
||||||
func (e *Editor) SelectionLen() int {
|
func (e *Editor) SelectionLen() int {
|
||||||
return abs(e.caret.start.ofs - e.caret.end.ofs)
|
return abs(e.caret.start.runes - e.caret.end.runes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Selection returns the start and end of the selection, as offsets into the
|
// Selection returns the start and end of the selection, as rune offsets.
|
||||||
// editor text. start can be > end.
|
// start can be > end.
|
||||||
func (e *Editor) Selection() (start, end int) {
|
func (e *Editor) Selection() (start, end int) {
|
||||||
return e.caret.start.ofs, e.caret.end.ofs
|
return e.caret.start.runes, e.caret.end.runes
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCaret moves the caret to start, and sets the selection end to end. start
|
// SetCaret moves the caret to start, and sets the selection end to end. start
|
||||||
// and end are in bytes, and represent offsets into the editor text. start and
|
// and end are in runes, and represent offsets into the editor text.
|
||||||
// end must be at a rune boundary.
|
|
||||||
func (e *Editor) SetCaret(start, end int) {
|
func (e *Editor) SetCaret(start, end int) {
|
||||||
e.makeValid()
|
e.makeValid()
|
||||||
// Constrain start and end to [0, e.Len()].
|
e.caret.start.runes, e.caret.end.runes = start, end
|
||||||
l := e.Len()
|
|
||||||
start = max(min(start, l), 0)
|
|
||||||
end = max(min(end, l), 0)
|
|
||||||
e.caret.start.ofs, e.caret.end.ofs = start, end
|
|
||||||
e.makeValidCaret()
|
e.makeValidCaret()
|
||||||
e.caret.scroll = true
|
e.caret.scroll = true
|
||||||
e.scroller.Stop()
|
e.scroller.Stop()
|
||||||
@@ -1340,24 +1294,17 @@ func (e *Editor) makeValidCaret(positions ...*combinedPos) {
|
|||||||
// Jump through some hoops to order the offsets given to offsetToScreenPos,
|
// Jump through some hoops to order the offsets given to offsetToScreenPos,
|
||||||
// but still be able to update them correctly with the results thereof.
|
// but still be able to update them correctly with the results thereof.
|
||||||
positions = append(positions, &e.caret.start, &e.caret.end)
|
positions = append(positions, &e.caret.start, &e.caret.end)
|
||||||
sort.Slice(positions, func(i, j int) bool {
|
|
||||||
return positions[i].ofs < positions[j].ofs
|
|
||||||
})
|
|
||||||
var hint combinedPos
|
|
||||||
for _, cp := range positions {
|
for _, cp := range positions {
|
||||||
hint = e.offsetToScreenPos(hint, cp.ofs)
|
*cp = e.closestPosition(combinedPos{runes: cp.runes})
|
||||||
*cp = hint
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SelectedText returns the currently selected text (if any) from the editor.
|
// SelectedText returns the currently selected text (if any) from the editor.
|
||||||
func (e *Editor) SelectedText() string {
|
func (e *Editor) SelectedText() string {
|
||||||
l := e.SelectionLen()
|
start := min(e.caret.start.ofs, e.caret.end.ofs)
|
||||||
if l == 0 {
|
end := max(e.caret.start.ofs, e.caret.end.ofs)
|
||||||
return ""
|
buf := make([]byte, end-start)
|
||||||
}
|
e.rr.Seek(int64(start), io.SeekStart)
|
||||||
buf := make([]byte, l)
|
|
||||||
e.rr.Seek(int64(min(e.caret.start.ofs, e.caret.end.ofs)), io.SeekStart)
|
|
||||||
_, err := e.rr.Read(buf)
|
_, err := e.rr.Read(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// The only error that rr.Read can return is EOF, which just means no
|
// The only error that rr.Read can return is EOF, which just means no
|
||||||
@@ -1440,13 +1387,13 @@ func nullLayout(r io.Reader) ([]text.Line, error) {
|
|||||||
var n int
|
var n int
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
for {
|
for {
|
||||||
r, s, err := rr.ReadRune()
|
r, _, err := rr.ReadRune()
|
||||||
n += s
|
|
||||||
buf.WriteRune(r)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rerr = err
|
rerr = err
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
n++
|
||||||
|
buf.WriteRune(r)
|
||||||
}
|
}
|
||||||
return []text.Line{
|
return []text.Line{
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"testing/quick"
|
"testing/quick"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
"gioui.org/font/gofont"
|
"gioui.org/font/gofont"
|
||||||
@@ -39,6 +40,9 @@ func TestEditor(t *testing.T) {
|
|||||||
e.SetCaret(0, 0) // shouldn't panic
|
e.SetCaret(0, 0) // shouldn't panic
|
||||||
assertCaret(t, e, 0, 0, 0)
|
assertCaret(t, e, 0, 0, 0)
|
||||||
e.SetText("æbc\naøå•")
|
e.SetText("æbc\naøå•")
|
||||||
|
if got, exp := e.Len(), utf8.RuneCountInString(e.Text()); got != exp {
|
||||||
|
t.Errorf("got length %d, expected %d", got, exp)
|
||||||
|
}
|
||||||
e.Layout(gtx, cache, font, fontSize, nil)
|
e.Layout(gtx, cache, font, fontSize, nil)
|
||||||
assertCaret(t, e, 0, 0, 0)
|
assertCaret(t, e, 0, 0, 0)
|
||||||
e.moveEnd(selectionClear)
|
e.moveEnd(selectionClear)
|
||||||
@@ -56,9 +60,9 @@ func TestEditor(t *testing.T) {
|
|||||||
|
|
||||||
e.SetCaret(0, 0)
|
e.SetCaret(0, 0)
|
||||||
assertCaret(t, e, 0, 0, 0)
|
assertCaret(t, e, 0, 0, 0)
|
||||||
e.SetCaret(len("æ"), len("æ"))
|
e.SetCaret(utf8.RuneCountInString("æ"), utf8.RuneCountInString("æ"))
|
||||||
assertCaret(t, e, 0, 1, 2)
|
assertCaret(t, e, 0, 1, 2)
|
||||||
e.SetCaret(len("æbc\naøå•"), len("æbc\naøå•"))
|
e.SetCaret(utf8.RuneCountInString("æbc\naøå•"), utf8.RuneCountInString("æbc\naøå•"))
|
||||||
assertCaret(t, e, 1, 4, len("æbc\naøå•"))
|
assertCaret(t, e, 1, 4, len("æbc\naøå•"))
|
||||||
|
|
||||||
// Ensure that password masking does not affect caret behavior
|
// Ensure that password masking does not affect caret behavior
|
||||||
|
|||||||
Reference in New Issue
Block a user