Files
8bitMyGoUI/pixel/edit.go
T

176 lines
3.4 KiB
Go

package pixel
import (
"slices"
"strings"
"time"
"unicode"
)
type snapshot struct {
text []rune
caret, anchor int
}
// editState is one Editor's text, caret and history.
type editState struct {
text []rune
caret, anchor int
compose []rune
composeCaret int
undo, redo []snapshot
changed bool
submitted bool
scroll int
dragging bool
pressAt time.Time
pressX, pressY float32
clicks int
}
func (s *editState) reset(v string) {
*s = editState{text: []rune(v)}
s.caret = len(s.text)
s.anchor = s.caret
}
func (s *editState) span() (int, int) { return min(s.caret, s.anchor), max(s.caret, s.anchor) }
func (s *editState) save() {
s.undo = append(s.undo, snapshot{slices.Clone(s.text), s.caret, s.anchor})
if len(s.undo) > 100 {
s.undo = s.undo[1:]
}
s.redo = nil
}
// replace puts r over the selection, within maxLen runes.
func (s *editState) replace(r []rune, maxLen int) {
a, b := s.span()
if maxLen > 0 {
r = r[:min(len(r), max(0, maxLen-len(s.text)+b-a))]
}
if a == b && len(r) == 0 {
return
}
s.save()
s.text = slices.Concat(s.text[:a], r, s.text[b:])
s.caret = a + len(r)
s.anchor = s.caret
s.changed = true
}
func (s *editState) erase(a, b int) {
a, b = max(0, min(a, b)), min(len(s.text), max(a, b))
if a == b {
return
}
s.save()
s.text = slices.Delete(slices.Clone(s.text), a, b)
s.caret, s.anchor = a, a
s.changed = true
}
// eraseOr erases the selection, or else a..b.
func (s *editState) eraseOr(a, b int) {
if x, y := s.span(); x != y {
a, b = x, y
}
s.erase(a, b)
}
func (s *editState) move(to int, extend bool) {
s.caret = max(0, min(to, len(s.text)))
if !extend {
s.anchor = s.caret
}
}
// restore pops a snapshot from one history onto the other.
func (s *editState) restore(from, to *[]snapshot) {
if len(*from) == 0 {
return
}
*to = append(*to, snapshot{slices.Clone(s.text), s.caret, s.anchor})
last := (*from)[len(*from)-1]
*from = (*from)[:len(*from)-1]
s.text, s.caret, s.anchor = last.text, last.caret, last.anchor
s.changed = true
}
func (s *editState) selected() string {
a, b := s.span()
return string(s.text[a:b])
}
// clean drops control runes; one line turns breaks into spaces.
func clean(t string, multiline bool) []rune {
var out []rune
for _, r := range strings.ReplaceAll(t, "\r\n", "\n") {
switch {
case r == '\n' && multiline:
out = append(out, r)
case r == '\n' || r == '\t' || r == '\r':
out = append(out, ' ')
case !unicode.IsControl(r):
out = append(out, r)
}
}
return out
}
func class(r rune) int {
switch {
case unicode.IsSpace(r):
return 0
case wide(r):
return 2
case unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_':
return 1
}
return 3
}
func wordRight(rs []rune, i int) int {
for i < len(rs) && class(rs[i]) == 0 {
i++
}
if i < len(rs) {
c := class(rs[i])
for i < len(rs) && class(rs[i]) == c {
i++
}
}
return i
}
func wordLeft(rs []rune, i int) int {
for i > 0 && class(rs[i-1]) == 0 {
i--
}
if i > 0 {
c := class(rs[i-1])
for i > 0 && class(rs[i-1]) == c {
i--
}
}
return i
}
// wordAt is the run of one class around rune i.
func wordAt(rs []rune, i int) (int, int) {
if len(rs) == 0 {
return 0, 0
}
i = min(i, len(rs)-1)
c := class(rs[i])
a, b := i, i+1
for a > 0 && class(rs[a-1]) == c {
a--
}
for b < len(rs) && class(rs[b]) == c {
b++
}
return a, b
}