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