github.com/utopiagio/gio@v0.0.8/widget/text.go (about) 1 package widget 2 3 import ( 4 "bufio" 5 "image" 6 "io" 7 "math" 8 "sort" 9 "unicode" 10 "unicode/utf8" 11 12 "github.com/utopiagio/gio/f32" 13 "github.com/utopiagio/gio/font" 14 "github.com/utopiagio/gio/layout" 15 "github.com/utopiagio/gio/op" 16 "github.com/utopiagio/gio/op/clip" 17 "github.com/utopiagio/gio/op/paint" 18 "github.com/utopiagio/gio/text" 19 "github.com/utopiagio/gio/unit" 20 21 "golang.org/x/exp/slices" 22 "golang.org/x/image/math/fixed" 23 ) 24 25 // textSource provides text data for use in widgets. If the underlying data type 26 // can fail due to I/O errors, it is the responsibility of that type to provide 27 // its own mechanism to surface and handle those errors. They will not always 28 // be returned by widgets using these functions. 29 type textSource interface { 30 io.ReaderAt 31 // Size returns the total length of the data in bytes. 32 Size() int64 33 // Changed returns whether the contents have changed since the last call 34 // to Changed. 35 Changed() bool 36 // ReplaceRunes replaces runeCount runes starting at byteOffset within the 37 // data with the provided string. Implementations of read-only text sources 38 // are free to make this a no-op. 39 ReplaceRunes(byteOffset int64, runeCount int64, replacement string) 40 } 41 42 // textView provides efficient shaping and indexing of interactive text. When provided 43 // with a TextSource, textView will shape and cache the runes within that source. 44 // It provides methods for configuring a viewport onto the shaped text which can 45 // be scrolled, and for configuring and drawing text selection boxes. 46 type textView struct { 47 Alignment text.Alignment 48 // LineHeight controls the distance between the baselines of lines of text. 49 // If zero, a sensible default will be used. 50 LineHeight unit.Sp 51 // LineHeightScale applies a scaling factor to the LineHeight. If zero, a 52 // sensible default will be used. 53 LineHeightScale float32 54 // SingleLine forces the text to stay on a single line. 55 // SingleLine also sets the scrolling direction to 56 // horizontal. 57 SingleLine bool 58 // MaxLines limits the shaped text to a specific quantity of shaped lines. 59 MaxLines int 60 // Truncator is the text that will be shown at the end of the final 61 // line if MaxLines is exceeded. Defaults to "…" if empty. 62 Truncator string 63 // WrapPolicy configures how displayed text will be broken into lines. 64 WrapPolicy text.WrapPolicy 65 // Mask replaces the visual display of each rune in the contents with the given rune. 66 // Newline characters are not masked. When non-zero, the unmasked contents 67 // are accessed by Len, Text, and SetText. 68 Mask rune 69 70 params text.Parameters 71 shaper *text.Shaper 72 seekCursor int64 73 rr textSource 74 maskReader maskReader 75 // graphemes tracks the indices of grapheme cluster boundaries within rr. 76 graphemes []int 77 // paragraphReader is used to populate graphemes. 78 paragraphReader graphemeReader 79 lastMask rune 80 viewSize image.Point 81 valid bool 82 regions []Region 83 dims layout.Dimensions 84 85 // offIndex is an index of rune index to byte offsets. 86 offIndex []offEntry 87 88 index glyphIndex 89 90 caret struct { 91 // xoff is the offset to the current position when moving between lines. 92 xoff fixed.Int26_6 93 // start is the current caret position in runes, and also the start position of 94 // selected text. end is the end position of selected text. If start 95 // == end, then there's no selection. Note that it's possible (and 96 // common) that the caret (start) is after the end, e.g. after 97 // Shift-DownArrow. 98 start int 99 end int 100 } 101 102 scrollOff image.Point 103 } 104 105 func (e *textView) Changed() bool { 106 return e.rr.Changed() 107 } 108 109 // Dimensions returns the dimensions of the visible text. 110 func (e *textView) Dimensions() layout.Dimensions { 111 basePos := e.dims.Size.Y - e.dims.Baseline 112 return layout.Dimensions{Size: e.viewSize, Baseline: e.viewSize.Y - basePos} 113 } 114 115 // FullDimensions returns the dimensions of all shaped text, including 116 // text that isn't visible within the current viewport. 117 func (e *textView) FullDimensions() layout.Dimensions { 118 return e.dims 119 } 120 121 // SetSource initializes the underlying data source for the Text. This 122 // must be done before invoking any other methods on Text. 123 func (e *textView) SetSource(source textSource) { 124 e.rr = source 125 e.invalidate() 126 e.seekCursor = 0 127 } 128 129 // ReadRuneAt reads the rune starting at the given byte offset, if any. 130 func (e *textView) ReadRuneAt(off int64) (rune, int, error) { 131 var buf [utf8.UTFMax]byte 132 b := buf[:] 133 n, err := e.rr.ReadAt(b, off) 134 b = b[:n] 135 r, s := utf8.DecodeRune(b) 136 return r, s, err 137 } 138 139 // ReadRuneAt reads the run prior to the given byte offset, if any. 140 func (e *textView) ReadRuneBefore(off int64) (rune, int, error) { 141 var buf [utf8.UTFMax]byte 142 b := buf[:] 143 if off < utf8.UTFMax { 144 b = b[:off] 145 off = 0 146 } else { 147 off -= utf8.UTFMax 148 } 149 n, err := e.rr.ReadAt(b, off) 150 b = b[:n] 151 r, s := utf8.DecodeLastRune(b) 152 return r, s, err 153 } 154 155 func (e *textView) makeValid() { 156 if e.valid { 157 return 158 } 159 e.layoutText(e.shaper) 160 e.valid = true 161 } 162 163 func (e *textView) closestToRune(runeIdx int) combinedPos { 164 e.makeValid() 165 pos, _ := e.index.closestToRune(runeIdx) 166 return pos 167 } 168 169 func (e *textView) closestToLineCol(line, col int) combinedPos { 170 e.makeValid() 171 return e.index.closestToLineCol(screenPos{line: line, col: col}) 172 } 173 174 func (e *textView) closestToXY(x fixed.Int26_6, y int) combinedPos { 175 e.makeValid() 176 return e.index.closestToXY(x, y) 177 } 178 179 func (e *textView) closestToXYGraphemes(x fixed.Int26_6, y int) combinedPos { 180 // Find the closest existing rune position to the provided coordinates. 181 pos := e.closestToXY(x, y) 182 // Resolve cluster boundaries on either side of the rune position. 183 firstOption := e.moveByGraphemes(pos.runes, 0) 184 distance := 1 185 if firstOption > pos.runes { 186 distance = -1 187 } 188 secondOption := e.moveByGraphemes(firstOption, distance) 189 // Choose the closest grapheme cluster boundary to the desired point. 190 first := e.closestToRune(firstOption) 191 firstDist := absFixed(first.x - x) 192 second := e.closestToRune(secondOption) 193 secondDist := absFixed(second.x - x) 194 if firstDist > secondDist { 195 return second 196 } else { 197 return first 198 } 199 } 200 201 func absFixed(i fixed.Int26_6) fixed.Int26_6 { 202 if i < 0 { 203 return -i 204 } 205 return i 206 } 207 208 // MaxLines moves the cursor the specified number of lines vertically, ensuring 209 // that the resulting position is aligned to a grapheme cluster. 210 func (e *textView) MoveLines(distance int, selAct selectionAction) { 211 caretStart := e.closestToRune(e.caret.start) 212 x := caretStart.x + e.caret.xoff 213 // Seek to line. 214 pos := e.closestToLineCol(caretStart.lineCol.line+distance, 0) 215 pos = e.closestToXYGraphemes(x, pos.y) 216 e.caret.start = pos.runes 217 e.caret.xoff = x - pos.x 218 e.updateSelection(selAct) 219 } 220 221 // calculateViewSize determines the size of the current visible content, 222 // ensuring that even if there is no text content, some space is reserved 223 // for the caret. 224 func (e *textView) calculateViewSize(gtx layout.Context) image.Point { 225 base := e.dims.Size 226 if caretWidth := e.caretWidth(gtx); base.X < caretWidth { 227 base.X = caretWidth 228 } 229 return gtx.Constraints.Constrain(base) 230 } 231 232 // Layout the text, reshaping it as necessary. 233 func (e *textView) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp) { 234 if e.params.Locale != gtx.Locale { 235 e.params.Locale = gtx.Locale 236 e.invalidate() 237 } 238 textSize := fixed.I(gtx.Sp(size)) 239 if e.params.Font != font || e.params.PxPerEm != textSize { 240 e.invalidate() 241 e.params.Font = font 242 e.params.PxPerEm = textSize 243 } 244 maxWidth := gtx.Constraints.Max.X 245 if e.SingleLine { 246 maxWidth = math.MaxInt 247 } 248 minWidth := gtx.Constraints.Min.X 249 if maxWidth != e.params.MaxWidth { 250 e.params.MaxWidth = maxWidth 251 e.invalidate() 252 } 253 if minWidth != e.params.MinWidth { 254 e.params.MinWidth = minWidth 255 e.invalidate() 256 } 257 if lt != e.shaper { 258 e.shaper = lt 259 e.invalidate() 260 } 261 if e.Mask != e.lastMask { 262 e.lastMask = e.Mask 263 e.invalidate() 264 } 265 if e.Alignment != e.params.Alignment { 266 e.params.Alignment = e.Alignment 267 e.invalidate() 268 } 269 if e.Truncator != e.params.Truncator { 270 e.params.Truncator = e.Truncator 271 e.invalidate() 272 } 273 if e.MaxLines != e.params.MaxLines { 274 e.params.MaxLines = e.MaxLines 275 e.invalidate() 276 } 277 if e.WrapPolicy != e.params.WrapPolicy { 278 e.params.WrapPolicy = e.WrapPolicy 279 e.invalidate() 280 } 281 if lh := fixed.I(gtx.Sp(e.LineHeight)); lh != e.params.LineHeight { 282 e.params.LineHeight = lh 283 e.invalidate() 284 } 285 if e.LineHeightScale != e.params.LineHeightScale { 286 e.params.LineHeightScale = e.LineHeightScale 287 e.invalidate() 288 } 289 290 e.makeValid() 291 292 if viewSize := e.calculateViewSize(gtx); viewSize != e.viewSize { 293 e.viewSize = viewSize 294 e.invalidate() 295 } 296 e.makeValid() 297 } 298 299 // PaintSelection clips and paints the visible text selection rectangles using 300 // the provided material to fill the rectangles. 301 func (e *textView) PaintSelection(gtx layout.Context, material op.CallOp) { 302 localViewport := image.Rectangle{Max: e.viewSize} 303 docViewport := image.Rectangle{Max: e.viewSize}.Add(e.scrollOff) 304 defer clip.Rect(localViewport).Push(gtx.Ops).Pop() 305 e.regions = e.index.locate(docViewport, e.caret.start, e.caret.end, e.regions) 306 for _, region := range e.regions { 307 area := clip.Rect(region.Bounds).Push(gtx.Ops) 308 material.Add(gtx.Ops) 309 paint.PaintOp{}.Add(gtx.Ops) 310 area.Pop() 311 } 312 } 313 314 // PaintText clips and paints the visible text glyph outlines using the provided 315 // material to fill the glyphs. 316 func (e *textView) PaintText(gtx layout.Context, material op.CallOp) { 317 m := op.Record(gtx.Ops) 318 viewport := image.Rectangle{ 319 Min: e.scrollOff, 320 Max: e.viewSize.Add(e.scrollOff), 321 } 322 it := textIterator{ 323 viewport: viewport, 324 material: material, 325 } 326 327 startGlyph := 0 328 for _, line := range e.index.lines { 329 if line.descent.Ceil()+line.yOff >= viewport.Min.Y { 330 break 331 } 332 startGlyph += line.glyphs 333 } 334 var glyphs [32]text.Glyph 335 line := glyphs[:0] 336 for _, g := range e.index.glyphs[startGlyph:] { 337 var ok bool 338 if line, ok = it.paintGlyph(gtx, e.shaper, g, line); !ok { 339 break 340 } 341 } 342 343 call := m.Stop() 344 viewport.Min = viewport.Min.Add(it.padding.Min) 345 viewport.Max = viewport.Max.Add(it.padding.Max) 346 defer clip.Rect(viewport.Sub(e.scrollOff)).Push(gtx.Ops).Pop() 347 call.Add(gtx.Ops) 348 } 349 350 // caretWidth returns the width occupied by the caret for the current 351 // gtx. 352 func (e *textView) caretWidth(gtx layout.Context) int { 353 carWidth2 := gtx.Dp(1) / 2 354 if carWidth2 < 1 { 355 carWidth2 = 1 356 } 357 return carWidth2 358 } 359 360 // PaintCaret clips and paints the caret rectangle, adding material immediately 361 // before painting to set the appropriate paint material. 362 func (e *textView) PaintCaret(gtx layout.Context, material op.CallOp) { 363 carWidth2 := e.caretWidth(gtx) 364 caretPos, carAsc, carDesc := e.CaretInfo() 365 366 carRect := image.Rectangle{ 367 Min: caretPos.Sub(image.Pt(carWidth2, carAsc)), 368 Max: caretPos.Add(image.Pt(carWidth2, carDesc)), 369 } 370 cl := image.Rectangle{Max: e.viewSize} 371 carRect = cl.Intersect(carRect) 372 if !carRect.Empty() { 373 defer clip.Rect(carRect).Push(gtx.Ops).Pop() 374 material.Add(gtx.Ops) 375 paint.PaintOp{}.Add(gtx.Ops) 376 } 377 } 378 379 func (e *textView) CaretInfo() (pos image.Point, ascent, descent int) { 380 caretStart := e.closestToRune(e.caret.start) 381 382 ascent = caretStart.ascent.Ceil() 383 descent = caretStart.descent.Ceil() 384 385 pos = image.Point{ 386 X: caretStart.x.Round(), 387 Y: caretStart.y, 388 } 389 pos = pos.Sub(e.scrollOff) 390 return 391 } 392 393 // ByteOffset returns the start byte of the rune at the given 394 // rune offset, clamped to the size of the text. 395 func (e *textView) ByteOffset(runeOffset int) int64 { 396 return int64(e.runeOffset(e.closestToRune(runeOffset).runes)) 397 } 398 399 // Len is the length of the editor contents, in runes. 400 func (e *textView) Len() int { 401 e.makeValid() 402 return e.closestToRune(math.MaxInt).runes 403 } 404 405 // Text returns the contents of the editor. If the provided buf is large enough, it will 406 // be filled and returned. Otherwise a new buffer will be allocated. 407 // Callers can guarantee that buf is large enough by giving it capacity e.Len()*utf8.UTFMax. 408 func (e *textView) Text(buf []byte) []byte { 409 size := e.rr.Size() 410 if cap(buf) < int(size) { 411 buf = make([]byte, size) 412 } 413 buf = buf[:size] 414 e.Seek(0, io.SeekStart) 415 n, _ := io.ReadFull(e, buf) 416 buf = buf[:n] 417 return buf 418 } 419 420 func (e *textView) ScrollBounds() image.Rectangle { 421 var b image.Rectangle 422 if e.SingleLine { 423 if len(e.index.lines) > 0 { 424 line := e.index.lines[0] 425 b.Min.X = line.xOff.Floor() 426 if b.Min.X > 0 { 427 b.Min.X = 0 428 } 429 } 430 b.Max.X = e.dims.Size.X + b.Min.X - e.viewSize.X 431 } else { 432 b.Max.Y = e.dims.Size.Y - e.viewSize.Y 433 } 434 return b 435 } 436 437 func (e *textView) ScrollRel(dx, dy int) { 438 e.scrollAbs(e.scrollOff.X+dx, e.scrollOff.Y+dy) 439 } 440 441 // ScrollOff returns the scroll offset of the text viewport. 442 func (e *textView) ScrollOff() image.Point { 443 return e.scrollOff 444 } 445 446 func (e *textView) scrollAbs(x, y int) { 447 e.scrollOff.X = x 448 e.scrollOff.Y = y 449 b := e.ScrollBounds() 450 if e.scrollOff.X > b.Max.X { 451 e.scrollOff.X = b.Max.X 452 } 453 if e.scrollOff.X < b.Min.X { 454 e.scrollOff.X = b.Min.X 455 } 456 if e.scrollOff.Y > b.Max.Y { 457 e.scrollOff.Y = b.Max.Y 458 } 459 if e.scrollOff.Y < b.Min.Y { 460 e.scrollOff.Y = b.Min.Y 461 } 462 } 463 464 // MoveCoord moves the caret to the position closest to the provided 465 // point that is aligned to a grapheme cluster boundary. 466 func (e *textView) MoveCoord(pos image.Point) { 467 x := fixed.I(pos.X + e.scrollOff.X) 468 y := pos.Y + e.scrollOff.Y 469 e.caret.start = e.closestToXYGraphemes(x, y).runes 470 e.caret.xoff = 0 471 } 472 473 // Truncated returns whether the text in the textView is currently 474 // truncated due to a restriction on the number of lines. 475 func (e *textView) Truncated() bool { 476 return e.index.truncated 477 } 478 479 func (e *textView) layoutText(lt *text.Shaper) { 480 e.Seek(0, io.SeekStart) 481 var r io.Reader = e 482 if e.Mask != 0 { 483 e.maskReader.Reset(e, e.Mask) 484 r = &e.maskReader 485 } 486 e.index.reset() 487 it := textIterator{viewport: image.Rectangle{Max: image.Point{X: math.MaxInt, Y: math.MaxInt}}} 488 if lt != nil { 489 lt.Layout(e.params, r) 490 for { 491 g, ok := lt.NextGlyph() 492 if !it.processGlyph(g, ok) { 493 break 494 } 495 e.index.Glyph(g) 496 } 497 } else { 498 // Make a fake glyph for every rune in the reader. 499 b := bufio.NewReader(r) 500 for _, _, err := b.ReadRune(); err != io.EOF; _, _, err = b.ReadRune() { 501 g := text.Glyph{Runes: 1, Flags: text.FlagClusterBreak} 502 _ = it.processGlyph(g, true) 503 e.index.Glyph(g) 504 } 505 } 506 e.paragraphReader.SetSource(e.rr) 507 e.graphemes = e.graphemes[:0] 508 for g := e.paragraphReader.Graphemes(); len(g) > 0; g = e.paragraphReader.Graphemes() { 509 if len(e.graphemes) > 0 && g[0] == e.graphemes[len(e.graphemes)-1] { 510 g = g[1:] 511 } 512 e.graphemes = append(e.graphemes, g...) 513 } 514 dims := layout.Dimensions{Size: it.bounds.Size()} 515 dims.Baseline = dims.Size.Y - it.baseline 516 e.dims = dims 517 } 518 519 // CaretPos returns the line & column numbers of the caret. 520 func (e *textView) CaretPos() (line, col int) { 521 pos := e.closestToRune(e.caret.start) 522 return pos.lineCol.line, pos.lineCol.col 523 } 524 525 // CaretCoords returns the coordinates of the caret, relative to the 526 // editor itself. 527 func (e *textView) CaretCoords() f32.Point { 528 pos := e.closestToRune(e.caret.start) 529 return f32.Pt(float32(pos.x)/64-float32(e.scrollOff.X), float32(pos.y-e.scrollOff.Y)) 530 } 531 532 // indexRune returns the latest rune index and byte offset no later than r. 533 func (e *textView) indexRune(r int) offEntry { 534 // Initialize index. 535 if len(e.offIndex) == 0 { 536 e.offIndex = append(e.offIndex, offEntry{}) 537 } 538 i := sort.Search(len(e.offIndex), func(i int) bool { 539 entry := e.offIndex[i] 540 return entry.runes >= r 541 }) 542 // Return the entry guaranteed to be less than or equal to r. 543 if i > 0 { 544 i-- 545 } 546 return e.offIndex[i] 547 } 548 549 // runeOffset returns the byte offset into e.rr of the r'th rune. 550 // r must be a valid rune index, usually returned by closestPosition. 551 func (e *textView) runeOffset(r int) int { 552 const runesPerIndexEntry = 50 553 entry := e.indexRune(r) 554 lastEntry := e.offIndex[len(e.offIndex)-1].runes 555 for entry.runes < r { 556 if entry.runes > lastEntry && entry.runes%runesPerIndexEntry == runesPerIndexEntry-1 { 557 e.offIndex = append(e.offIndex, entry) 558 } 559 _, s, _ := e.ReadRuneAt(int64(entry.bytes)) 560 entry.bytes += s 561 entry.runes++ 562 } 563 return entry.bytes 564 } 565 566 func (e *textView) invalidate() { 567 e.offIndex = e.offIndex[:0] 568 e.valid = false 569 } 570 571 // Replace the text between start and end with s. Indices are in runes. 572 // It returns the number of runes inserted. 573 func (e *textView) Replace(start, end int, s string) int { 574 if start > end { 575 start, end = end, start 576 } 577 startPos := e.closestToRune(start) 578 endPos := e.closestToRune(end) 579 startOff := e.runeOffset(startPos.runes) 580 replaceSize := endPos.runes - startPos.runes 581 sc := utf8.RuneCountInString(s) 582 newEnd := startPos.runes + sc 583 584 e.rr.ReplaceRunes(int64(startOff), int64(replaceSize), s) 585 adjust := func(pos int) int { 586 switch { 587 case newEnd < pos && pos <= endPos.runes: 588 pos = newEnd 589 case endPos.runes < pos: 590 diff := newEnd - endPos.runes 591 pos = pos + diff 592 } 593 return pos 594 } 595 e.caret.start = adjust(e.caret.start) 596 e.caret.end = adjust(e.caret.end) 597 e.invalidate() 598 return sc 599 } 600 601 // MovePages moves the caret position by vertical pages of text, ensuring that 602 // the final position is aligned to a grapheme cluster boundary. 603 func (e *textView) MovePages(pages int, selAct selectionAction) { 604 caret := e.closestToRune(e.caret.start) 605 x := caret.x + e.caret.xoff 606 y := caret.y + pages*e.viewSize.Y 607 pos := e.closestToXYGraphemes(x, y) 608 e.caret.start = pos.runes 609 e.caret.xoff = x - pos.x 610 e.updateSelection(selAct) 611 } 612 613 // moveByGraphemes returns the rune index resulting from moving the 614 // specified number of grapheme clusters from startRuneidx. 615 func (e *textView) moveByGraphemes(startRuneidx, graphemes int) int { 616 if len(e.graphemes) == 0 { 617 return startRuneidx 618 } 619 startGraphemeIdx, _ := slices.BinarySearch(e.graphemes, startRuneidx) 620 startGraphemeIdx = max(startGraphemeIdx+graphemes, 0) 621 startGraphemeIdx = min(startGraphemeIdx, len(e.graphemes)-1) 622 startRuneIdx := e.graphemes[startGraphemeIdx] 623 return e.closestToRune(startRuneIdx).runes 624 } 625 626 // clampCursorToGraphemes ensures that the final start/end positions of 627 // the cursor are on grapheme cluster boundaries. 628 func (e *textView) clampCursorToGraphemes() { 629 e.caret.start = e.moveByGraphemes(e.caret.start, 0) 630 e.caret.end = e.moveByGraphemes(e.caret.end, 0) 631 } 632 633 // MoveCaret moves the caret (aka selection start) and the selection end 634 // relative to their current positions. Positive distances moves forward, 635 // negative distances moves backward. Distances are in grapheme clusters which 636 // better match the expectations of users than runes. 637 func (e *textView) MoveCaret(startDelta, endDelta int) { 638 e.caret.xoff = 0 639 e.caret.start = e.moveByGraphemes(e.caret.start, startDelta) 640 e.caret.end = e.moveByGraphemes(e.caret.end, endDelta) 641 } 642 643 // MoveStart moves the caret to the start of the current line, ensuring that the resulting 644 // cursor position is on a grapheme cluster boundary. 645 func (e *textView) MoveStart(selAct selectionAction) { 646 caret := e.closestToRune(e.caret.start) 647 caret = e.closestToLineCol(caret.lineCol.line, 0) 648 e.caret.start = caret.runes 649 e.caret.xoff = -caret.x 650 e.updateSelection(selAct) 651 e.clampCursorToGraphemes() 652 } 653 654 // MoveEnd moves the caret to the end of the current line, ensuring that the resulting 655 // cursor position is on a grapheme cluster boundary. 656 func (e *textView) MoveEnd(selAct selectionAction) { 657 caret := e.closestToRune(e.caret.start) 658 caret = e.closestToLineCol(caret.lineCol.line, math.MaxInt) 659 e.caret.start = caret.runes 660 e.caret.xoff = fixed.I(e.params.MaxWidth) - caret.x 661 e.updateSelection(selAct) 662 e.clampCursorToGraphemes() 663 } 664 665 // MoveWord moves the caret to the next word in the specified direction. 666 // Positive is forward, negative is backward. 667 // Absolute values greater than one will skip that many words. 668 // The final caret position will be aligned to a grapheme cluster boundary. 669 // BUG(whereswaldon): this method's definition of a "word" is currently 670 // whitespace-delimited. Languages that do not use whitespace to delimit 671 // words will experience counter-intuitive behavior when navigating by 672 // word. 673 func (e *textView) MoveWord(distance int, selAct selectionAction) { 674 // split the distance information into constituent parts to be 675 // used independently. 676 words, direction := distance, 1 677 if distance < 0 { 678 words, direction = distance*-1, -1 679 } 680 // atEnd if caret is at either side of the buffer. 681 caret := e.closestToRune(e.caret.start) 682 atEnd := func() bool { 683 return caret.runes == 0 || caret.runes == e.Len() 684 } 685 // next returns the appropriate rune given the direction. 686 next := func() (r rune) { 687 off := e.runeOffset(caret.runes) 688 if direction < 0 { 689 r, _, _ = e.ReadRuneBefore(int64(off)) 690 } else { 691 r, _, _ = e.ReadRuneAt(int64(off)) 692 } 693 return r 694 } 695 for ii := 0; ii < words; ii++ { 696 for r := next(); unicode.IsSpace(r) && !atEnd(); r = next() { 697 e.MoveCaret(direction, 0) 698 caret = e.closestToRune(e.caret.start) 699 } 700 e.MoveCaret(direction, 0) 701 caret = e.closestToRune(e.caret.start) 702 for r := next(); !unicode.IsSpace(r) && !atEnd(); r = next() { 703 e.MoveCaret(direction, 0) 704 caret = e.closestToRune(e.caret.start) 705 } 706 } 707 e.updateSelection(selAct) 708 e.clampCursorToGraphemes() 709 } 710 711 func (e *textView) ScrollToCaret() { 712 caret := e.closestToRune(e.caret.start) 713 if e.SingleLine { 714 var dist int 715 if d := caret.x.Floor() - e.scrollOff.X; d < 0 { 716 dist = d 717 } else if d := caret.x.Ceil() - (e.scrollOff.X + e.viewSize.X); d > 0 { 718 dist = d 719 } 720 e.ScrollRel(dist, 0) 721 } else { 722 miny := caret.y - caret.ascent.Ceil() 723 maxy := caret.y + caret.descent.Ceil() 724 var dist int 725 if d := miny - e.scrollOff.Y; d < 0 { 726 dist = d 727 } else if d := maxy - (e.scrollOff.Y + e.viewSize.Y); d > 0 { 728 dist = d 729 } 730 e.ScrollRel(0, dist) 731 } 732 } 733 734 // SelectionLen returns the length of the selection, in runes; it is 735 // equivalent to utf8.RuneCountInString(e.SelectedText()). 736 func (e *textView) SelectionLen() int { 737 return abs(e.caret.start - e.caret.end) 738 } 739 740 // Selection returns the start and end of the selection, as rune offsets. 741 // start can be > end. 742 func (e *textView) Selection() (start, end int) { 743 return e.caret.start, e.caret.end 744 } 745 746 // SetCaret moves the caret to start, and sets the selection end to end. Then 747 // the two ends are clamped to the nearest grapheme cluster boundary. start 748 // and end are in runes, and represent offsets into the editor text. 749 func (e *textView) SetCaret(start, end int) { 750 e.caret.start = e.closestToRune(start).runes 751 e.caret.end = e.closestToRune(end).runes 752 e.clampCursorToGraphemes() 753 } 754 755 // SelectedText returns the currently selected text (if any) from the editor, 756 // filling the provided byte slice if it is large enough or allocating and 757 // returning a new byte slice if the provided one is insufficient. 758 // Callers can guarantee that the buf is large enough by providing a buffer 759 // with capacity e.SelectionLen()*utf8.UTFMax. 760 func (e *textView) SelectedText(buf []byte) []byte { 761 startOff := e.runeOffset(e.caret.start) 762 endOff := e.runeOffset(e.caret.end) 763 start := min(startOff, endOff) 764 end := max(startOff, endOff) 765 if cap(buf) < end-start { 766 buf = make([]byte, end-start) 767 } 768 buf = buf[:end-start] 769 n, _ := e.rr.ReadAt(buf, int64(start)) 770 // There is no way to reasonably handle a read error here. We rely upon 771 // implementations of textSource to provide other ways to signal errors 772 // if the user cares about that, and here we use whatever data we were 773 // able to read. 774 return buf[:n] 775 } 776 777 func (e *textView) updateSelection(selAct selectionAction) { 778 if selAct == selectionClear { 779 e.ClearSelection() 780 } 781 } 782 783 // ClearSelection clears the selection, by setting the selection end equal to 784 // the selection start. 785 func (e *textView) ClearSelection() { 786 e.caret.end = e.caret.start 787 } 788 789 // WriteTo implements io.WriterTo. 790 func (e *textView) WriteTo(w io.Writer) (int64, error) { 791 e.Seek(0, io.SeekStart) 792 return io.Copy(w, struct{ io.Reader }{e}) 793 } 794 795 // Seek implements io.Seeker. 796 func (e *textView) Seek(offset int64, whence int) (int64, error) { 797 switch whence { 798 case io.SeekStart: 799 e.seekCursor = offset 800 case io.SeekCurrent: 801 e.seekCursor += offset 802 case io.SeekEnd: 803 e.seekCursor = e.rr.Size() + offset 804 } 805 return e.seekCursor, nil 806 } 807 808 // Read implements io.Reader. 809 func (e *textView) Read(p []byte) (int, error) { 810 n, err := e.rr.ReadAt(p, e.seekCursor) 811 e.seekCursor += int64(n) 812 return n, err 813 } 814 815 // ReadAt implements io.ReaderAt. 816 func (e *textView) ReadAt(p []byte, offset int64) (int, error) { 817 return e.rr.ReadAt(p, offset) 818 } 819 820 // Regions returns visible regions covering the rune range [start,end). 821 func (e *textView) Regions(start, end int, regions []Region) []Region { 822 viewport := image.Rectangle{ 823 Min: e.scrollOff, 824 Max: e.viewSize.Add(e.scrollOff), 825 } 826 return e.index.locate(viewport, start, end, regions) 827 }