mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
e98c8955bb
This commit rebuilds the editor and label types on the common foundation provided by textView. This enables labels to have optional state that makes them selectable, and allows the two widgets to share the code for managing cursor positions, displaying selections, and soforth. Labels now have an additional Layout function which can be invoked if they have a Selectable. It accepts a layout.Widget used to paint their contents. Stateless labels should still use the old Layout method. Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
132 lines
2.9 KiB
Go
132 lines
2.9 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package widget
|
|
|
|
import (
|
|
"io"
|
|
"unicode/utf8"
|
|
|
|
"golang.org/x/text/runes"
|
|
)
|
|
|
|
// editBuffer implements a gap buffer for text editing.
|
|
type editBuffer struct {
|
|
// pos is the byte position for Read and ReadRune.
|
|
pos int
|
|
|
|
// The gap start and end in bytes.
|
|
gapstart, gapend int
|
|
text []byte
|
|
|
|
// changed tracks whether the buffer content
|
|
// has changed since the last call to Changed.
|
|
changed bool
|
|
}
|
|
|
|
var _ textSource = (*editBuffer)(nil)
|
|
|
|
const minSpace = 5
|
|
|
|
func (e *editBuffer) Changed() bool {
|
|
c := e.changed
|
|
e.changed = false
|
|
return c
|
|
}
|
|
|
|
func (e *editBuffer) deleteRunes(caret, count int) (bytes int, runes int) {
|
|
e.moveGap(caret, 0)
|
|
for ; count < 0 && e.gapstart > 0; count++ {
|
|
_, s := utf8.DecodeLastRune(e.text[:e.gapstart])
|
|
e.gapstart -= s
|
|
bytes += s
|
|
runes++
|
|
e.changed = e.changed || s > 0
|
|
}
|
|
for ; count > 0 && e.gapend < len(e.text); count-- {
|
|
_, s := utf8.DecodeRune(e.text[e.gapend:])
|
|
e.gapend += s
|
|
e.changed = e.changed || s > 0
|
|
}
|
|
return
|
|
}
|
|
|
|
// moveGap moves the gap to the caret position. After returning,
|
|
// the gap is guaranteed to be at least space bytes long.
|
|
func (e *editBuffer) moveGap(caret, space int) {
|
|
if e.gapLen() < space {
|
|
if space < minSpace {
|
|
space = minSpace
|
|
}
|
|
txt := make([]byte, int(e.Size())+space)
|
|
// Expand to capacity.
|
|
txt = txt[:cap(txt)]
|
|
gaplen := len(txt) - int(e.Size())
|
|
if caret > e.gapstart {
|
|
copy(txt, e.text[:e.gapstart])
|
|
copy(txt[caret+gaplen:], e.text[caret:])
|
|
copy(txt[e.gapstart:], e.text[e.gapend:caret+e.gapLen()])
|
|
} else {
|
|
copy(txt, e.text[:caret])
|
|
copy(txt[e.gapstart+gaplen:], e.text[e.gapend:])
|
|
copy(txt[caret+gaplen:], e.text[caret:e.gapstart])
|
|
}
|
|
e.text = txt
|
|
e.gapstart = caret
|
|
e.gapend = e.gapstart + gaplen
|
|
} else {
|
|
if caret > e.gapstart {
|
|
copy(e.text[e.gapstart:], e.text[e.gapend:caret+e.gapLen()])
|
|
} else {
|
|
copy(e.text[caret+e.gapLen():], e.text[caret:e.gapstart])
|
|
}
|
|
l := e.gapLen()
|
|
e.gapstart = caret
|
|
e.gapend = e.gapstart + l
|
|
}
|
|
}
|
|
|
|
func (e *editBuffer) Size() int64 {
|
|
return int64(len(e.text) - e.gapLen())
|
|
}
|
|
|
|
func (e *editBuffer) gapLen() int {
|
|
return e.gapend - e.gapstart
|
|
}
|
|
|
|
func (e *editBuffer) ReadAt(p []byte, offset int64) (int, error) {
|
|
if len(p) == 0 {
|
|
return 0, nil
|
|
}
|
|
if offset == e.Size() {
|
|
return 0, io.EOF
|
|
}
|
|
var total int
|
|
if offset < int64(e.gapstart) {
|
|
n := copy(p, e.text[offset:e.gapstart])
|
|
p = p[n:]
|
|
total += n
|
|
offset += int64(n)
|
|
}
|
|
if offset >= int64(e.gapstart) {
|
|
n := copy(p, e.text[offset+int64(e.gapLen()):])
|
|
total += n
|
|
}
|
|
return total, nil
|
|
}
|
|
|
|
func (e *editBuffer) ReplaceRunes(byteOffset, runeCount int64, s string) {
|
|
e.deleteRunes(int(byteOffset), int(runeCount))
|
|
e.prepend(int(byteOffset), s)
|
|
}
|
|
|
|
func (e *editBuffer) prepend(caret int, s string) {
|
|
if !utf8.ValidString(s) {
|
|
s = runes.ReplaceIllFormed().String(s)
|
|
}
|
|
|
|
e.moveGap(caret, len(s))
|
|
copy(e.text[caret:], s)
|
|
e.gapstart += len(s)
|
|
e.changed = e.changed || len(s) > 0
|
|
}
|