mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
widget: create standalone textView
This commit adds a standalone state type for manipulating and displaying text. It reads text from a minimal interface, shapes it, tracks valid cursor positions, and provides sizing and scrolling services to higher-level widgets. My long term goal with these types is to export them to allow non-core widgets to build atop them, but I've left them private for now. Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit is contained in:
+2
-2
@@ -68,7 +68,7 @@ type Editor struct {
|
||||
maxWidth, minWidth int
|
||||
viewSize image.Point
|
||||
valid bool
|
||||
regions []region
|
||||
regions []Region
|
||||
dims layout.Dimensions
|
||||
requestFocus bool
|
||||
|
||||
@@ -750,7 +750,7 @@ func (e *Editor) PaintSelection(gtx layout.Context) {
|
||||
defer clip.Rect(localViewport).Push(gtx.Ops).Pop()
|
||||
e.regions = e.index.locate(docViewport, e.caret.start, e.caret.end, e.regions)
|
||||
for _, region := range e.regions {
|
||||
area := clip.Rect(region.bounds.Sub(e.scrollOff)).Push(gtx.Ops)
|
||||
area := clip.Rect(region.Bounds).Push(gtx.Ops)
|
||||
paint.PaintOp{}.Add(gtx.Ops)
|
||||
area.Pop()
|
||||
}
|
||||
|
||||
+18
-11
@@ -281,34 +281,38 @@ func (g *glyphIndex) closestToXY(x fixed.Int26_6, y int) combinedPos {
|
||||
// makeRegion creates a text-aligned rectangle from start to end. The vertical
|
||||
// dimensions of the rectangle are derived from the provided line's ascent and
|
||||
// descent, and the y offset of the line's baseline is provided as y.
|
||||
func makeRegion(line lineInfo, y int, start, end fixed.Int26_6) region {
|
||||
func makeRegion(line lineInfo, y int, start, end fixed.Int26_6) Region {
|
||||
if start > end {
|
||||
start, end = end, start
|
||||
}
|
||||
dotStart := image.Pt(start.Round(), y)
|
||||
dotEnd := image.Pt(end.Round(), y)
|
||||
return region{
|
||||
bounds: image.Rectangle{
|
||||
return Region{
|
||||
Bounds: image.Rectangle{
|
||||
Min: dotStart.Sub(image.Point{Y: line.ascent.Ceil()}),
|
||||
Max: dotEnd.Add(image.Point{Y: line.descent.Floor()}),
|
||||
},
|
||||
baseline: line.descent.Floor(),
|
||||
Baseline: line.descent.Floor(),
|
||||
}
|
||||
}
|
||||
|
||||
// region describes an area of interest within shaped text.
|
||||
type region struct {
|
||||
// bounds is the coordinates of the bounding box in document space.
|
||||
bounds image.Rectangle
|
||||
// baseline is the quantity of vertical pixels between the baseline and
|
||||
// Region describes the position and baseline of an area of interest within
|
||||
// shaped text.
|
||||
type Region struct {
|
||||
// Bounds is the coordinates of the bounding box relative to the containing
|
||||
// widget.
|
||||
Bounds image.Rectangle
|
||||
// Baseline is the quantity of vertical pixels between the baseline and
|
||||
// the bottom of bounds.
|
||||
baseline int
|
||||
Baseline int
|
||||
}
|
||||
|
||||
// locate returns highlight regions covering the glyphs that represent the runes in
|
||||
// [startRune,endRune). If the rects parameter is non-nil, locate will use it to
|
||||
// return results instead of allocating, provided that there is enough capacity.
|
||||
func (g *glyphIndex) locate(viewport image.Rectangle, startRune, endRune int, rects []region) []region {
|
||||
// The returned regions have their Bounds specified relative to the provided
|
||||
// viewport.
|
||||
func (g *glyphIndex) locate(viewport image.Rectangle, startRune, endRune int, rects []Region) []Region {
|
||||
if startRune > endRune {
|
||||
startRune, endRune = endRune, startRune
|
||||
}
|
||||
@@ -388,5 +392,8 @@ func (g *glyphIndex) locate(viewport image.Rectangle, startRune, endRune int, re
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := range rects {
|
||||
rects[i].Bounds = rects[i].Bounds.Sub(viewport.Min)
|
||||
}
|
||||
return rects
|
||||
}
|
||||
|
||||
+735
@@ -0,0 +1,735 @@
|
||||
package widget
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"image"
|
||||
"io"
|
||||
"math"
|
||||
"sort"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
// textSource provides text data for use in widgets. If the underlying data type
|
||||
// can fail due to I/O errors, it is the responsibility of that type to provide
|
||||
// its own mechanism to surface and handle those errors. They will not always
|
||||
// be returned by widgets using these functions.
|
||||
type textSource interface {
|
||||
io.ReaderAt
|
||||
// Size returns the total length of the data in bytes.
|
||||
Size() int64
|
||||
// Changed returns whether the contents have changed since the last call
|
||||
// to Changed.
|
||||
Changed() bool
|
||||
// ReplaceRunes replaces runeCount runes starting at byteOffset within the
|
||||
// data with the provided string. Implementations of read-only text sources
|
||||
// are free to make this a no-op.
|
||||
ReplaceRunes(byteOffset int64, runeCount int64, replacement string)
|
||||
}
|
||||
|
||||
type maskReader2 struct {
|
||||
// rr is the underlying reader.
|
||||
rr io.RuneReader
|
||||
maskBuf [utf8.UTFMax]byte
|
||||
// mask is the utf-8 encoded mask rune.
|
||||
mask []byte
|
||||
// overflow contains excess mask bytes left over after the last Read call.
|
||||
overflow []byte
|
||||
}
|
||||
|
||||
func (m *maskReader2) Reset(r io.Reader, mr rune) {
|
||||
m.rr = bufio.NewReader(r)
|
||||
n := utf8.EncodeRune(m.maskBuf[:], mr)
|
||||
m.mask = m.maskBuf[:n]
|
||||
}
|
||||
|
||||
// Read reads from the underlying reader and replaces every
|
||||
// rune with the mask rune.
|
||||
func (m *maskReader2) Read(b []byte) (n int, err error) {
|
||||
for len(b) > 0 {
|
||||
var replacement []byte
|
||||
if len(m.overflow) > 0 {
|
||||
replacement = m.overflow
|
||||
} else {
|
||||
var r rune
|
||||
r, _, err = m.rr.ReadRune()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if r == '\n' {
|
||||
replacement = []byte{'\n'}
|
||||
} else {
|
||||
replacement = m.mask
|
||||
}
|
||||
}
|
||||
nn := copy(b, replacement)
|
||||
m.overflow = replacement[nn:]
|
||||
n += nn
|
||||
b = b[nn:]
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// textView provides efficient shaping and indexing of interactive text. When provided
|
||||
// with a TextSource, textView will shape and cache the runes within that source.
|
||||
// It provides methods for configuring a viewport onto the shaped text which can
|
||||
// be scrolled, and for configuring and drawing text selection boxes.
|
||||
type textView struct {
|
||||
Alignment text.Alignment
|
||||
// SingleLine forces the text to stay on a single line.
|
||||
// SingleLine also sets the scrolling direction to
|
||||
// horizontal.
|
||||
SingleLine bool
|
||||
// MaxLines limits the shaped text to a specific quantity of shaped lines.
|
||||
MaxLines int
|
||||
// Mask replaces the visual display of each rune in the contents with the given rune.
|
||||
// Newline characters are not masked. When non-zero, the unmasked contents
|
||||
// are accessed by Len, Text, and SetText.
|
||||
Mask rune
|
||||
|
||||
font text.Font
|
||||
shaper *text.Shaper
|
||||
textSize fixed.Int26_6
|
||||
seekCursor int64
|
||||
rr textSource
|
||||
maskReader maskReader2
|
||||
lastMask rune
|
||||
maxWidth, minWidth int
|
||||
viewSize image.Point
|
||||
valid bool
|
||||
regions []Region
|
||||
dims layout.Dimensions
|
||||
|
||||
// offIndex is an index of rune index to byte offsets.
|
||||
offIndex []offEntry
|
||||
|
||||
index glyphIndex
|
||||
|
||||
caret struct {
|
||||
// xoff is the offset to the current position when moving between lines.
|
||||
xoff fixed.Int26_6
|
||||
// start is the current caret position in runes, and also the start position of
|
||||
// selected text. end is the end position of selected text. If start
|
||||
// == end, then there's no selection. Note that it's possible (and
|
||||
// common) that the caret (start) is after the end, e.g. after
|
||||
// Shift-DownArrow.
|
||||
start int
|
||||
end int
|
||||
}
|
||||
|
||||
scrollOff image.Point
|
||||
|
||||
locale system.Locale
|
||||
}
|
||||
|
||||
func (e *textView) Changed() bool {
|
||||
return e.rr.Changed()
|
||||
}
|
||||
|
||||
// Dimensions returns the dimensions of the visible text.
|
||||
func (e *textView) Dimensions() layout.Dimensions {
|
||||
basePos := e.dims.Size.Y - e.dims.Baseline
|
||||
return layout.Dimensions{Size: e.viewSize, Baseline: e.viewSize.Y - basePos}
|
||||
}
|
||||
|
||||
// FullDimensions returns the dimensions of all shaped text, including
|
||||
// text that isn't visible within the current viewport.
|
||||
func (e *textView) FullDimensions() layout.Dimensions {
|
||||
return e.dims
|
||||
}
|
||||
|
||||
// SetSource initializes the underlying data source for the Text. This
|
||||
// must be done before invoking any other methods on Text.
|
||||
func (e *textView) SetSource(source textSource) {
|
||||
e.rr = source
|
||||
e.invalidate()
|
||||
e.seekCursor = 0
|
||||
}
|
||||
|
||||
// ReadRuneAt reads the rune starting at the given byte offset, if any.
|
||||
func (e *textView) ReadRuneAt(off int64) (rune, int, error) {
|
||||
var buf [utf8.UTFMax]byte
|
||||
b := buf[:]
|
||||
n, err := e.rr.ReadAt(b, off)
|
||||
b = b[:n]
|
||||
r, s := utf8.DecodeRune(b)
|
||||
return r, s, err
|
||||
}
|
||||
|
||||
// ReadRuneAt reads the run prior to the given byte offset, if any.
|
||||
func (e *textView) ReadRuneBefore(off int64) (rune, int, error) {
|
||||
var buf [utf8.UTFMax]byte
|
||||
b := buf[:]
|
||||
if off < utf8.UTFMax {
|
||||
b = b[:off]
|
||||
off = 0
|
||||
} else {
|
||||
off -= utf8.UTFMax
|
||||
}
|
||||
n, err := e.rr.ReadAt(b, off)
|
||||
b = b[:n]
|
||||
r, s := utf8.DecodeLastRune(b)
|
||||
return r, s, err
|
||||
}
|
||||
|
||||
func (e *textView) makeValid() {
|
||||
if e.valid {
|
||||
return
|
||||
}
|
||||
e.layoutText(e.shaper)
|
||||
e.valid = true
|
||||
}
|
||||
|
||||
func (e *textView) closestToRune(runeIdx int) combinedPos {
|
||||
e.makeValid()
|
||||
pos, _ := e.index.closestToRune(runeIdx)
|
||||
return pos
|
||||
}
|
||||
|
||||
func (e *textView) closestToLineCol(line, col int) combinedPos {
|
||||
e.makeValid()
|
||||
return e.index.closestToLineCol(screenPos{line: line, col: col})
|
||||
}
|
||||
|
||||
func (e *textView) closestToXY(x fixed.Int26_6, y int) combinedPos {
|
||||
e.makeValid()
|
||||
return e.index.closestToXY(x, y)
|
||||
}
|
||||
|
||||
func (e *textView) MoveLines(distance int, selAct selectionAction) {
|
||||
caretStart := e.closestToRune(e.caret.start)
|
||||
x := caretStart.x + e.caret.xoff
|
||||
// Seek to line.
|
||||
pos := e.closestToLineCol(caretStart.lineCol.line+distance, 0)
|
||||
pos = e.closestToXY(x, pos.y)
|
||||
e.caret.start = pos.runes
|
||||
e.caret.xoff = x - pos.x
|
||||
e.updateSelection(selAct)
|
||||
}
|
||||
|
||||
// calculateViewSize determines the size of the current visible content,
|
||||
// ensuring that even if there is no text content, some space is reserved
|
||||
// for the caret.
|
||||
func (e *textView) calculateViewSize(gtx layout.Context) image.Point {
|
||||
base := e.dims.Size
|
||||
if caretWidth := e.caretWidth(gtx); base.X < caretWidth {
|
||||
base.X = caretWidth
|
||||
}
|
||||
return gtx.Constraints.Constrain(base)
|
||||
}
|
||||
|
||||
// Update the text, reshaping it as necessary. If not nil, eventHandling will be invoked after reshaping the text to
|
||||
// allow parent widgets to adapt to any changes in text content or positioning. If eventHandling invalidates the
|
||||
// Text, Update will ensure that it is valid again before returning.
|
||||
func (e *textView) Update(gtx layout.Context, lt *text.Shaper, font text.Font, size unit.Sp, eventHandling func(gtx layout.Context)) {
|
||||
if e.locale != gtx.Locale {
|
||||
e.locale = gtx.Locale
|
||||
e.invalidate()
|
||||
}
|
||||
textSize := fixed.I(gtx.Sp(size))
|
||||
if e.font != font || e.textSize != textSize {
|
||||
e.invalidate()
|
||||
e.font = font
|
||||
e.textSize = textSize
|
||||
}
|
||||
maxWidth := gtx.Constraints.Max.X
|
||||
if e.SingleLine {
|
||||
maxWidth = math.MaxInt
|
||||
}
|
||||
minWidth := gtx.Constraints.Min.X
|
||||
if maxWidth != e.maxWidth {
|
||||
e.maxWidth = maxWidth
|
||||
e.invalidate()
|
||||
}
|
||||
if minWidth != e.minWidth {
|
||||
e.minWidth = minWidth
|
||||
e.invalidate()
|
||||
}
|
||||
if lt != e.shaper {
|
||||
e.shaper = lt
|
||||
e.invalidate()
|
||||
}
|
||||
if e.Mask != e.lastMask {
|
||||
e.lastMask = e.Mask
|
||||
e.invalidate()
|
||||
}
|
||||
|
||||
e.makeValid()
|
||||
if eventHandling != nil {
|
||||
eventHandling(gtx)
|
||||
e.makeValid()
|
||||
}
|
||||
|
||||
if viewSize := e.calculateViewSize(gtx); viewSize != e.viewSize {
|
||||
e.viewSize = viewSize
|
||||
e.invalidate()
|
||||
}
|
||||
e.makeValid()
|
||||
}
|
||||
|
||||
// PaintSelection paints the contrasting background for selected text.
|
||||
func (e *textView) PaintSelection(gtx layout.Context) {
|
||||
localViewport := image.Rectangle{Max: e.viewSize}
|
||||
docViewport := image.Rectangle{Max: e.viewSize}.Add(e.scrollOff)
|
||||
defer clip.Rect(localViewport).Push(gtx.Ops).Pop()
|
||||
e.regions = e.index.locate(docViewport, e.caret.start, e.caret.end, e.regions)
|
||||
for _, region := range e.regions {
|
||||
area := clip.Rect(region.Bounds).Push(gtx.Ops)
|
||||
paint.PaintOp{}.Add(gtx.Ops)
|
||||
area.Pop()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *textView) PaintText(gtx layout.Context) {
|
||||
m := op.Record(gtx.Ops)
|
||||
viewport := image.Rectangle{
|
||||
Min: e.scrollOff,
|
||||
Max: e.viewSize.Add(e.scrollOff),
|
||||
}
|
||||
it := textIterator{viewport: viewport}
|
||||
|
||||
startGlyph := 0
|
||||
for _, line := range e.index.lines {
|
||||
if line.descent.Ceil()+line.yOff >= viewport.Min.Y {
|
||||
break
|
||||
}
|
||||
startGlyph += line.glyphs
|
||||
}
|
||||
var glyphs [32]text.Glyph
|
||||
line := glyphs[:0]
|
||||
for _, g := range e.index.glyphs[startGlyph:] {
|
||||
var ok bool
|
||||
if line, ok = it.paintGlyph(gtx, e.shaper, g, line); !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
call := m.Stop()
|
||||
viewport.Min = viewport.Min.Add(it.padding.Min)
|
||||
viewport.Max = viewport.Max.Add(it.padding.Max)
|
||||
defer clip.Rect(viewport.Sub(e.scrollOff)).Push(gtx.Ops).Pop()
|
||||
call.Add(gtx.Ops)
|
||||
}
|
||||
|
||||
// caretWidth returns the width occupied by the caret for the current
|
||||
// gtx.
|
||||
func (e *textView) caretWidth(gtx layout.Context) int {
|
||||
carWidth2 := gtx.Dp(1) / 2
|
||||
if carWidth2 < 1 {
|
||||
carWidth2 = 1
|
||||
}
|
||||
return carWidth2
|
||||
}
|
||||
|
||||
func (e *textView) PaintCaret(gtx layout.Context) {
|
||||
carWidth2 := e.caretWidth(gtx)
|
||||
caretPos, carAsc, carDesc := e.CaretInfo()
|
||||
|
||||
carRect := image.Rectangle{
|
||||
Min: caretPos.Sub(image.Pt(carWidth2, carAsc)),
|
||||
Max: caretPos.Add(image.Pt(carWidth2, carDesc)),
|
||||
}
|
||||
cl := image.Rectangle{Max: e.viewSize}
|
||||
carRect = cl.Intersect(carRect)
|
||||
if !carRect.Empty() {
|
||||
defer clip.Rect(carRect).Push(gtx.Ops).Pop()
|
||||
paint.PaintOp{}.Add(gtx.Ops)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *textView) CaretInfo() (pos image.Point, ascent, descent int) {
|
||||
caretStart := e.closestToRune(e.caret.start)
|
||||
|
||||
ascent = caretStart.ascent.Ceil()
|
||||
descent = caretStart.descent.Ceil()
|
||||
|
||||
pos = image.Point{
|
||||
X: caretStart.x.Round(),
|
||||
Y: caretStart.y,
|
||||
}
|
||||
pos = pos.Sub(e.scrollOff)
|
||||
return
|
||||
}
|
||||
|
||||
// ByteOffset returns the offset of the start byte of the rune nearest
|
||||
// to the rune at the given offset. If the given offset is before or
|
||||
// after the text, it will be clamped to the first or last rune.
|
||||
func (e *textView) ByteOffset(runeOffset int) int64 {
|
||||
return int64(e.runeOffset(e.closestToRune(runeOffset).runes))
|
||||
}
|
||||
|
||||
// Len is the length of the editor contents, in runes.
|
||||
func (e *textView) Len() int {
|
||||
e.makeValid()
|
||||
return e.closestToRune(math.MaxInt).runes
|
||||
}
|
||||
|
||||
// Text returns the contents of the editor.
|
||||
func (e *textView) Text() string {
|
||||
e.Seek(0, io.SeekStart)
|
||||
b, _ := io.ReadAll(e)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (e *textView) ScrollBounds() image.Rectangle {
|
||||
var b image.Rectangle
|
||||
if e.SingleLine {
|
||||
if len(e.index.lines) > 0 {
|
||||
line := e.index.lines[0]
|
||||
b.Min.X = line.xOff.Floor()
|
||||
if b.Min.X > 0 {
|
||||
b.Min.X = 0
|
||||
}
|
||||
}
|
||||
b.Max.X = e.dims.Size.X + b.Min.X - e.viewSize.X
|
||||
} else {
|
||||
b.Max.Y = e.dims.Size.Y - e.viewSize.Y
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (e *textView) ScrollRel(dx, dy int) {
|
||||
e.scrollAbs(e.scrollOff.X+dx, e.scrollOff.Y+dy)
|
||||
}
|
||||
|
||||
// ScrollOff returns the scroll offset of the text viewport.
|
||||
func (e *textView) ScrollOff() image.Point {
|
||||
return e.scrollOff
|
||||
}
|
||||
|
||||
func (e *textView) scrollAbs(x, y int) {
|
||||
e.scrollOff.X = x
|
||||
e.scrollOff.Y = y
|
||||
b := e.ScrollBounds()
|
||||
if e.scrollOff.X > b.Max.X {
|
||||
e.scrollOff.X = b.Max.X
|
||||
}
|
||||
if e.scrollOff.X < b.Min.X {
|
||||
e.scrollOff.X = b.Min.X
|
||||
}
|
||||
if e.scrollOff.Y > b.Max.Y {
|
||||
e.scrollOff.Y = b.Max.Y
|
||||
}
|
||||
if e.scrollOff.Y < b.Min.Y {
|
||||
e.scrollOff.Y = b.Min.Y
|
||||
}
|
||||
}
|
||||
|
||||
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.closestToXY(x, y).runes
|
||||
e.caret.xoff = 0
|
||||
}
|
||||
|
||||
func (e *textView) layoutText(lt *text.Shaper) {
|
||||
e.Seek(0, io.SeekStart)
|
||||
var r io.Reader = e
|
||||
if e.Mask != 0 {
|
||||
e.maskReader.Reset(e, e.Mask)
|
||||
r = &e.maskReader
|
||||
}
|
||||
e.index = glyphIndex{}
|
||||
it := textIterator{viewport: image.Rectangle{Max: image.Point{X: math.MaxInt, Y: math.MaxInt}}}
|
||||
if lt != nil {
|
||||
lt.LayoutReader(text.Parameters{
|
||||
Font: e.font,
|
||||
PxPerEm: e.textSize,
|
||||
Alignment: e.Alignment,
|
||||
MaxLines: e.MaxLines,
|
||||
}, e.minWidth, e.maxWidth, e.locale, r)
|
||||
for glyph, ok := it.processGlyph(lt.NextGlyph()); ok; glyph, ok = it.processGlyph(lt.NextGlyph()) {
|
||||
e.index.Glyph(glyph)
|
||||
}
|
||||
} else {
|
||||
// Make a fake glyph for every rune in the reader.
|
||||
b := bufio.NewReader(r)
|
||||
for _, _, err := b.ReadRune(); err != io.EOF; _, _, err = b.ReadRune() {
|
||||
g, _ := it.processGlyph(text.Glyph{Runes: 1, Flags: text.FlagClusterBreak}, true)
|
||||
e.index.Glyph(g)
|
||||
|
||||
}
|
||||
}
|
||||
dims := layout.Dimensions{Size: it.bounds.Size()}
|
||||
dims.Baseline = dims.Size.Y - it.baseline
|
||||
e.dims = dims
|
||||
}
|
||||
|
||||
// CaretPos returns the line & column numbers of the caret.
|
||||
func (e *textView) CaretPos() (line, col int) {
|
||||
pos := e.closestToRune(e.caret.start)
|
||||
return pos.lineCol.line, pos.lineCol.col
|
||||
}
|
||||
|
||||
// CaretCoords returns the coordinates of the caret, relative to the
|
||||
// editor itself.
|
||||
func (e *textView) CaretCoords() f32.Point {
|
||||
pos := e.closestToRune(e.caret.start)
|
||||
return f32.Pt(float32(pos.x)/64-float32(e.scrollOff.X), float32(pos.y-e.scrollOff.Y))
|
||||
}
|
||||
|
||||
// indexRune returns the latest rune index and byte offset no later than r.
|
||||
func (e *textView) indexRune(r int) offEntry {
|
||||
// Initialize index.
|
||||
if len(e.offIndex) == 0 {
|
||||
e.offIndex = append(e.offIndex, offEntry{})
|
||||
}
|
||||
i := sort.Search(len(e.offIndex), func(i int) bool {
|
||||
entry := e.offIndex[i]
|
||||
return entry.runes >= r
|
||||
})
|
||||
// Return the entry guaranteed to be less than or equal to r.
|
||||
if i > 0 {
|
||||
i--
|
||||
}
|
||||
return e.offIndex[i]
|
||||
}
|
||||
|
||||
// runeOffset returns the byte offset into e.rr of the r'th rune.
|
||||
// r must be a valid rune index, usually returned by closestPosition.
|
||||
func (e *textView) runeOffset(r int) int {
|
||||
const runesPerIndexEntry = 50
|
||||
entry := e.indexRune(r)
|
||||
lastEntry := e.offIndex[len(e.offIndex)-1].runes
|
||||
for entry.runes < r {
|
||||
if entry.runes > lastEntry && entry.runes%runesPerIndexEntry == runesPerIndexEntry-1 {
|
||||
e.offIndex = append(e.offIndex, entry)
|
||||
}
|
||||
_, s, _ := e.ReadRuneAt(int64(entry.bytes))
|
||||
entry.bytes += s
|
||||
entry.runes++
|
||||
}
|
||||
return entry.bytes
|
||||
}
|
||||
|
||||
func (e *textView) invalidate() {
|
||||
e.offIndex = e.offIndex[:0]
|
||||
e.valid = false
|
||||
}
|
||||
|
||||
// Replace the text between start and end with s. Indices are in runes.
|
||||
// It returns the number of runes inserted.
|
||||
// addHistory controls whether this modification is recorded in the undo
|
||||
// history. Replace can modify text in positions unrelated to the cursor
|
||||
// position.
|
||||
func (e *textView) Replace(start, end int, s string) int {
|
||||
if start > end {
|
||||
start, end = end, start
|
||||
}
|
||||
startPos := e.closestToRune(start)
|
||||
endPos := e.closestToRune(end)
|
||||
startOff := e.runeOffset(startPos.runes)
|
||||
replaceSize := endPos.runes - startPos.runes
|
||||
sc := utf8.RuneCountInString(s)
|
||||
newEnd := startPos.runes + sc
|
||||
|
||||
e.rr.ReplaceRunes(int64(startOff), int64(replaceSize), s)
|
||||
adjust := func(pos int) int {
|
||||
switch {
|
||||
case newEnd < pos && pos <= endPos.runes:
|
||||
pos = newEnd
|
||||
case endPos.runes < pos:
|
||||
diff := newEnd - endPos.runes
|
||||
pos = pos + diff
|
||||
}
|
||||
return pos
|
||||
}
|
||||
e.caret.start = adjust(e.caret.start)
|
||||
e.caret.end = adjust(e.caret.end)
|
||||
e.invalidate()
|
||||
return sc
|
||||
}
|
||||
|
||||
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.closestToXY(x, y)
|
||||
e.caret.start = pos.runes
|
||||
e.caret.xoff = x - pos.x
|
||||
e.updateSelection(selAct)
|
||||
}
|
||||
|
||||
// MoveCaret moves the caret (aka selection start) and the selection end
|
||||
// relative to their current positions. Positive distances moves forward,
|
||||
// negative distances moves backward. Distances are in runes.
|
||||
func (e *textView) MoveCaret(startDelta, endDelta int) {
|
||||
e.caret.xoff = 0
|
||||
e.caret.start = e.closestToRune(e.caret.start + startDelta).runes
|
||||
e.caret.end = e.closestToRune(e.caret.end + endDelta).runes
|
||||
}
|
||||
|
||||
func (e *textView) MoveStart(selAct selectionAction) {
|
||||
caret := e.closestToRune(e.caret.start)
|
||||
caret = e.closestToLineCol(caret.lineCol.line, 0)
|
||||
e.caret.start = caret.runes
|
||||
e.caret.xoff = -caret.x
|
||||
e.updateSelection(selAct)
|
||||
}
|
||||
|
||||
func (e *textView) MoveEnd(selAct selectionAction) {
|
||||
caret := e.closestToRune(e.caret.start)
|
||||
caret = e.closestToLineCol(caret.lineCol.line, math.MaxInt)
|
||||
e.caret.start = caret.runes
|
||||
e.caret.xoff = fixed.I(e.maxWidth) - caret.x
|
||||
e.updateSelection(selAct)
|
||||
}
|
||||
|
||||
// MoveWord moves the caret to the next word in the specified direction.
|
||||
// Positive is forward, negative is backward.
|
||||
// Absolute values greater than one will skip that many words.
|
||||
func (e *textView) MoveWord(distance int, selAct selectionAction) {
|
||||
// split the distance information into constituent parts to be
|
||||
// used independently.
|
||||
words, direction := distance, 1
|
||||
if distance < 0 {
|
||||
words, direction = distance*-1, -1
|
||||
}
|
||||
// atEnd if caret is at either side of the buffer.
|
||||
caret := e.closestToRune(e.caret.start)
|
||||
atEnd := func() bool {
|
||||
return caret.runes == 0 || caret.runes == e.Len()
|
||||
}
|
||||
// next returns the appropriate rune given the direction.
|
||||
next := func() (r rune) {
|
||||
off := e.runeOffset(caret.runes)
|
||||
if direction < 0 {
|
||||
r, _, _ = e.ReadRuneBefore(int64(off))
|
||||
} else {
|
||||
r, _, _ = e.ReadRuneAt(int64(off))
|
||||
}
|
||||
return r
|
||||
}
|
||||
for ii := 0; ii < words; ii++ {
|
||||
for r := next(); unicode.IsSpace(r) && !atEnd(); r = next() {
|
||||
e.MoveCaret(direction, 0)
|
||||
caret = e.closestToRune(e.caret.start)
|
||||
}
|
||||
e.MoveCaret(direction, 0)
|
||||
caret = e.closestToRune(e.caret.start)
|
||||
for r := next(); !unicode.IsSpace(r) && !atEnd(); r = next() {
|
||||
e.MoveCaret(direction, 0)
|
||||
caret = e.closestToRune(e.caret.start)
|
||||
}
|
||||
}
|
||||
e.updateSelection(selAct)
|
||||
}
|
||||
|
||||
func (e *textView) ScrollToCaret() {
|
||||
caret := e.closestToRune(e.caret.start)
|
||||
if e.SingleLine {
|
||||
var dist int
|
||||
if d := caret.x.Floor() - e.scrollOff.X; d < 0 {
|
||||
dist = d
|
||||
} else if d := caret.x.Ceil() - (e.scrollOff.X + e.viewSize.X); d > 0 {
|
||||
dist = d
|
||||
}
|
||||
e.ScrollRel(dist, 0)
|
||||
} else {
|
||||
miny := caret.y - caret.ascent.Ceil()
|
||||
maxy := caret.y + caret.descent.Ceil()
|
||||
var dist int
|
||||
if d := miny - e.scrollOff.Y; d < 0 {
|
||||
dist = d
|
||||
} else if d := maxy - (e.scrollOff.Y + e.viewSize.Y); d > 0 {
|
||||
dist = d
|
||||
}
|
||||
e.ScrollRel(0, dist)
|
||||
}
|
||||
}
|
||||
|
||||
// SelectionLen returns the length of the selection, in runes; it is
|
||||
// equivalent to utf8.RuneCountInString(e.SelectedText()).
|
||||
func (e *textView) SelectionLen() int {
|
||||
return abs(e.caret.start - e.caret.end)
|
||||
}
|
||||
|
||||
// Selection returns the start and end of the selection, as rune offsets.
|
||||
// start can be > end.
|
||||
func (e *textView) Selection() (start, end int) {
|
||||
return e.caret.start, e.caret.end
|
||||
}
|
||||
|
||||
// SetCaret moves the caret to start, and sets the selection end to end. start
|
||||
// and end are in runes, and represent offsets into the editor text.
|
||||
func (e *textView) SetCaret(start, end int) {
|
||||
e.caret.start = e.closestToRune(start).runes
|
||||
e.caret.end = e.closestToRune(end).runes
|
||||
}
|
||||
|
||||
// SelectedText returns the currently selected text (if any) from the editor.
|
||||
func (e *textView) SelectedText() string {
|
||||
startOff := e.runeOffset(e.caret.start)
|
||||
endOff := e.runeOffset(e.caret.end)
|
||||
start := min(startOff, endOff)
|
||||
end := max(startOff, endOff)
|
||||
buf := make([]byte, end-start)
|
||||
n, _ := e.rr.ReadAt(buf, int64(start))
|
||||
// There is no way to reasonably handle a read error here. We rely upon
|
||||
// implementations of textSource to provide other ways to signal errors
|
||||
// if the user cares about that, and here we use whatever data we were
|
||||
// able to read.
|
||||
return string(buf[:n])
|
||||
}
|
||||
|
||||
func (e *textView) updateSelection(selAct selectionAction) {
|
||||
if selAct == selectionClear {
|
||||
e.ClearSelection()
|
||||
}
|
||||
}
|
||||
|
||||
// ClearSelection clears the selection, by setting the selection end equal to
|
||||
// the selection start.
|
||||
func (e *textView) ClearSelection() {
|
||||
e.caret.end = e.caret.start
|
||||
}
|
||||
|
||||
// WriteTo implements io.WriterTo.
|
||||
func (e *textView) WriteTo(w io.Writer) (int64, error) {
|
||||
e.Seek(0, io.SeekStart)
|
||||
return io.Copy(w, struct{ io.Reader }{e})
|
||||
}
|
||||
|
||||
// Seek implements io.Seeker.
|
||||
func (e *textView) Seek(offset int64, whence int) (int64, error) {
|
||||
switch whence {
|
||||
case io.SeekStart:
|
||||
e.seekCursor = offset
|
||||
case io.SeekCurrent:
|
||||
e.seekCursor += offset
|
||||
case io.SeekEnd:
|
||||
e.seekCursor = e.rr.Size() + offset
|
||||
}
|
||||
return e.seekCursor, nil
|
||||
}
|
||||
|
||||
// Read implements io.Reader.
|
||||
func (e *textView) Read(p []byte) (int, error) {
|
||||
n, err := e.rr.ReadAt(p, e.seekCursor)
|
||||
e.seekCursor += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// ReadAt implements io.ReaderAt.
|
||||
func (e *textView) ReadAt(p []byte, offset int64) (int, error) {
|
||||
return e.rr.ReadAt(p, offset)
|
||||
}
|
||||
|
||||
// Regions returns visible regions covering the rune range [start,end).
|
||||
func (e *textView) Regions(start, end int, regions []Region) []Region {
|
||||
viewport := image.Rectangle{
|
||||
Min: e.scrollOff,
|
||||
Max: e.viewSize.Add(e.scrollOff),
|
||||
}
|
||||
return e.index.locate(viewport, start, end, regions)
|
||||
}
|
||||
Reference in New Issue
Block a user