github.com/gop9/olt@v0.0.0-20200202132135-d956aad50b08/framework/text.go (about) 1 package framework 2 3 import ( 4 "image" 5 "image/color" 6 "math" 7 "runtime" 8 "time" 9 "unicode" 10 11 gkey "github.com/gop9/olt/gio/io/key" 12 13 "github.com/gop9/olt/framework/clipboard" 14 "github.com/gop9/olt/framework/command" 15 "github.com/gop9/olt/framework/font" 16 "github.com/gop9/olt/framework/label" 17 "github.com/gop9/olt/framework/rect" 18 nstyle "github.com/gop9/olt/framework/style" 19 "github.com/gop9/olt/gio/io/pointer" 20 ) 21 22 /////////////////////////////////////////////////////////////////////////////////// 23 // TEXT EDITOR 24 /////////////////////////////////////////////////////////////////////////////////// 25 26 type propertyStatus int 27 28 const ( 29 propertyDefault = propertyStatus(iota) 30 propertyEdit 31 propertyDrag 32 ) 33 34 // TextEditor stores the state of a text editor. 35 // To add a text editor to a window create a TextEditor object with 36 // &TextEditor{}, store it somewhere then in the update function call 37 // the Edit method passing the window to it. 38 type TextEditor struct { 39 win *Window 40 propertyStatus propertyStatus 41 Cursor int 42 Buffer []rune 43 Filter FilterFunc 44 Flags EditFlags 45 CursorFollow bool 46 Redraw bool 47 48 Maxlen int 49 50 PasswordChar rune // if non-zero all characters are displayed like this character 51 52 Initialized bool 53 Active bool 54 InsertMode bool 55 Scrollbar image.Point 56 SelectStart, SelectEnd int 57 HasPreferredX bool 58 SingleLine bool 59 PreferredX int 60 Undo textUndoState 61 62 drawchunks []drawchunk 63 64 lastClickCoord image.Point 65 lastClickTime time.Time 66 clickCount int 67 trueSelectStart int 68 69 needle []rune 70 71 password []rune // support buffer for drawing PasswordChar!=0 fields 72 } 73 74 type drawchunk struct { 75 rect.Rect 76 start, end int 77 } 78 79 func (ed *TextEditor) init(win *Window) { 80 if ed.Filter == nil { 81 ed.Filter = FilterDefault 82 } 83 if !ed.Initialized { 84 if ed.Flags&EditMultiline != 0 { 85 ed.clearState(TextEditMultiLine) 86 } else { 87 ed.clearState(TextEditSingleLine) 88 } 89 90 } 91 if ed.win == nil || ed.win != win { 92 if ed.win == nil { 93 if ed.Buffer == nil { 94 ed.Buffer = []rune{} 95 } 96 ed.Filter = nil 97 ed.Cursor = 0 98 } 99 ed.Redraw = true 100 ed.win = win 101 } 102 } 103 104 type EditFlags int 105 106 const ( 107 EditDefault EditFlags = 0 108 EditReadOnly EditFlags = 1 << iota 109 EditAutoSelect 110 EditSigEnter 111 EditNoCursor 112 EditSelectable 113 EditClipboard 114 EditCtrlEnterNewline 115 EditNoHorizontalScroll 116 EditAlwaysInsertMode 117 EditMultiline 118 EditNeverInsertMode 119 EditFocusFollowsMouse 120 EditNoContextMenu 121 EditIbeamCursor 122 123 EditSimple = EditAlwaysInsertMode 124 EditField = EditSelectable | EditClipboard | EditSigEnter 125 EditBox = EditSelectable | EditMultiline | EditClipboard 126 ) 127 128 type EditEvents int 129 130 const ( 131 EditActive EditEvents = 1 << iota 132 EditInactive 133 EditActivated 134 EditDeactivated 135 EditCommitted 136 ) 137 138 type TextEditType int 139 140 const ( 141 TextEditSingleLine TextEditType = iota 142 TextEditMultiLine 143 ) 144 145 type textUndoRecord struct { 146 Where int 147 InsertLength int 148 DeleteLength int 149 Text []rune 150 } 151 152 const _TEXTEDIT_UNDOSTATECOUNT = 99 153 154 type textUndoState struct { 155 UndoRec [_TEXTEDIT_UNDOSTATECOUNT]textUndoRecord 156 UndoPoint int16 157 RedoPoint int16 158 } 159 160 func strInsertText(str []rune, pos int, runes []rune) []rune { 161 if cap(str) < len(str)+len(runes) { 162 newcap := (cap(str) + 1) * 2 163 if newcap < len(str)+len(runes) { 164 newcap = len(str) + len(runes) 165 } 166 newstr := make([]rune, len(str), newcap) 167 copy(newstr, str) 168 str = newstr 169 } 170 str = str[:len(str)+len(runes)] 171 copy(str[pos+len(runes):], str[pos:]) 172 copy(str[pos:], runes) 173 return str 174 } 175 176 func strDeleteText(s []rune, pos int, dlen int) []rune { 177 copy(s[pos:], s[pos+dlen:]) 178 s = s[:len(s)-dlen] 179 return s 180 } 181 182 func (s *TextEditor) hasSelection() bool { 183 return s.SelectStart != s.SelectEnd 184 } 185 186 func (edit *TextEditor) locateCoord(p image.Point, font font.Face, row_height int) int { 187 x, y := p.X, p.Y 188 189 var drawchunk *drawchunk 190 191 for i := range edit.drawchunks { 192 min := edit.drawchunks[i].Min() 193 max := edit.drawchunks[i].Max() 194 getprev := false 195 if min.Y <= y && y <= max.Y { 196 if min.X <= x && x <= max.X { 197 drawchunk = &edit.drawchunks[i] 198 } 199 if min.X > x { 200 getprev = true 201 } 202 } else if min.Y > y { 203 getprev = true 204 } 205 if getprev { 206 if i == 0 { 207 drawchunk = &edit.drawchunks[0] 208 } else { 209 drawchunk = &edit.drawchunks[i-1] 210 } 211 break 212 } 213 } 214 215 if drawchunk == nil { 216 return len(edit.Buffer) 217 } 218 219 curx := drawchunk.X 220 for i := drawchunk.start; i < drawchunk.end && i < len(edit.Buffer); i++ { 221 curx += FontWidth(font, string(edit.Buffer[i:i+1])) 222 if curx > x { 223 return i 224 } 225 } 226 227 if drawchunk.end >= len(edit.Buffer) { 228 return len(edit.Buffer) 229 } 230 231 return drawchunk.end 232 } 233 234 func (edit *TextEditor) indexToCoord(index int, font font.Face, row_height int) image.Point { 235 var drawchunk *drawchunk 236 237 for i := range edit.drawchunks { 238 if edit.drawchunks[i].start > index { 239 if i == 0 { 240 drawchunk = &edit.drawchunks[0] 241 } else { 242 drawchunk = &edit.drawchunks[i-1] 243 } 244 break 245 } 246 } 247 248 if drawchunk == nil { 249 if len(edit.drawchunks) == 0 { 250 return image.Point{} 251 } 252 drawchunk = &edit.drawchunks[len(edit.drawchunks)-1] 253 } 254 255 x := drawchunk.X 256 for i := drawchunk.start; i < drawchunk.end && i < len(edit.Buffer); i++ { 257 if i >= index { 258 break 259 } 260 x += FontWidth(font, string(edit.Buffer[i:i+1])) 261 } 262 if index >= len(edit.Buffer) && len(edit.Buffer) > 0 && edit.Buffer[len(edit.Buffer)-1] == '\n' { 263 return image.Point{x, drawchunk.Y + drawchunk.H + drawchunk.H/2} 264 } 265 return image.Point{x, drawchunk.Y + drawchunk.H/2} 266 } 267 268 func (state *TextEditor) doubleClick(coord image.Point) bool { 269 abs := func(x int) int { 270 if x < 0 { 271 return -x 272 } 273 return x 274 } 275 r := time.Since(state.lastClickTime) < 200*time.Millisecond && abs(state.lastClickCoord.X-coord.X) < 5 && abs(state.lastClickCoord.Y-coord.Y) < 5 276 state.lastClickCoord = coord 277 state.lastClickTime = time.Now() 278 return r 279 280 } 281 282 func (state *TextEditor) click(coord image.Point, font font.Face, row_height int) { 283 /* API click: on mouse down, move the cursor to the clicked location, 284 * and reset the selection */ 285 state.Cursor = state.locateCoord(coord, font, row_height) 286 287 state.SelectStart = state.Cursor 288 state.trueSelectStart = state.Cursor 289 state.SelectEnd = state.Cursor 290 state.HasPreferredX = false 291 292 switch state.clickCount { 293 case 2: 294 state.selectWord(state.SelectEnd) 295 case 3: 296 state.selectLine(state.SelectEnd) 297 } 298 } 299 300 func (state *TextEditor) drag(coord image.Point, font font.Face, row_height int) { 301 /* API drag: on mouse drag, move the cursor and selection endpoint 302 * to the clicked location */ 303 var p int = state.locateCoord(coord, font, row_height) 304 if state.SelectStart == state.SelectEnd { 305 state.SelectStart = state.Cursor 306 state.trueSelectStart = p 307 } 308 state.SelectEnd = p 309 state.Cursor = state.SelectEnd 310 311 switch state.clickCount { 312 case 2: 313 state.selectWord(p) 314 case 3: 315 state.selectLine(p) 316 } 317 } 318 319 func (state *TextEditor) selectWord(end int) { 320 state.SelectStart = state.trueSelectStart 321 state.SelectEnd = end 322 state.sortselection() 323 state.SelectStart = state.towd(state.SelectStart, -1, false) 324 state.SelectEnd = state.towd(state.SelectEnd, +1, true) 325 } 326 327 func (state *TextEditor) selectLine(end int) { 328 state.SelectStart = state.trueSelectStart 329 state.SelectEnd = end 330 state.sortselection() 331 state.SelectStart = state.tonl(state.SelectStart-1, -1) 332 state.SelectEnd = state.tonl(state.SelectEnd, +1) 333 } 334 335 func (state *TextEditor) clamp() { 336 /* make the selection/cursor state valid if client altered the string */ 337 if state.hasSelection() { 338 if state.SelectStart > len(state.Buffer) { 339 state.SelectStart = len(state.Buffer) 340 } 341 if state.SelectEnd > len(state.Buffer) { 342 state.SelectEnd = len(state.Buffer) 343 } 344 345 /* if clamping forced them to be equal, move the cursor to match */ 346 if state.SelectStart == state.SelectEnd { 347 state.Cursor = state.SelectStart 348 } 349 } 350 351 if state.Cursor > len(state.Buffer) { 352 state.Cursor = len(state.Buffer) 353 } 354 } 355 356 // Deletes a chunk of text in the editor. 357 func (edit *TextEditor) Delete(where int, len int) { 358 /* delete characters while updating undo */ 359 edit.makeundoDelete(where, len) 360 361 edit.Buffer = strDeleteText(edit.Buffer, where, len) 362 edit.HasPreferredX = false 363 } 364 365 // Deletes selection. 366 func (edit *TextEditor) DeleteSelection() { 367 /* delete the section */ 368 edit.clamp() 369 370 if edit.hasSelection() { 371 if edit.SelectStart < edit.SelectEnd { 372 edit.Delete(edit.SelectStart, edit.SelectEnd-edit.SelectStart) 373 edit.Cursor = edit.SelectStart 374 edit.SelectEnd = edit.Cursor 375 } else { 376 edit.Delete(edit.SelectEnd, edit.SelectStart-edit.SelectEnd) 377 edit.Cursor = edit.SelectEnd 378 edit.SelectStart = edit.Cursor 379 } 380 381 edit.HasPreferredX = false 382 } 383 } 384 385 func (state *TextEditor) sortselection() { 386 /* canonicalize the selection so start <= end */ 387 if state.SelectEnd < state.SelectStart { 388 var temp int = state.SelectEnd 389 state.SelectEnd = state.SelectStart 390 state.SelectStart = temp 391 } 392 } 393 394 func (state *TextEditor) moveToFirst() { 395 /* move cursor to first character of selection */ 396 if state.hasSelection() { 397 state.sortselection() 398 state.Cursor = state.SelectStart 399 state.SelectEnd = state.SelectStart 400 state.HasPreferredX = false 401 } 402 } 403 404 func (state *TextEditor) moveToLast() { 405 /* move cursor to last character of selection */ 406 if state.hasSelection() { 407 state.sortselection() 408 state.clamp() 409 state.Cursor = state.SelectEnd 410 state.SelectStart = state.SelectEnd 411 state.HasPreferredX = false 412 } 413 } 414 415 // Moves to the beginning or end of a line 416 func (state *TextEditor) tonl(start int, dir int) int { 417 sz := len(state.Buffer) 418 419 i := start 420 if i < 0 { 421 return 0 422 } 423 if i >= sz { 424 i = sz - 1 425 } 426 for ; (i >= 0) && (i < sz); i += dir { 427 c := state.Buffer[i] 428 429 if c == '\n' { 430 if dir >= 0 { 431 return i 432 } else { 433 return i + 1 434 } 435 } 436 } 437 if dir < 0 { 438 return 0 439 } else { 440 return sz 441 } 442 } 443 444 // Moves to the beginning or end of an alphanumerically delimited word 445 func (state *TextEditor) towd(start int, dir int, dontForceAdvance bool) int { 446 first := (dir < 0) 447 notfirst := !first 448 var i int 449 for i = start; (i >= 0) && (i < len(state.Buffer)); i += dir { 450 c := state.Buffer[i] 451 if !(unicode.IsLetter(c) || unicode.IsDigit(c) || (c == '_')) { 452 if !first && !dontForceAdvance { 453 i++ 454 } 455 break 456 } 457 first = notfirst 458 } 459 if i < 0 { 460 i = 0 461 } 462 return i 463 } 464 465 func (state *TextEditor) prepSelectionAtCursor() { 466 /* update selection and cursor to match each other */ 467 if !state.hasSelection() { 468 state.SelectEnd = state.Cursor 469 state.SelectStart = state.SelectEnd 470 } else { 471 state.Cursor = state.SelectEnd 472 } 473 } 474 475 func (edit *TextEditor) Cut() int { 476 if edit.Flags&EditReadOnly != 0 { 477 return 0 478 } 479 /* API cut: delete selection */ 480 if edit.hasSelection() { 481 edit.DeleteSelection() /* implicitly clamps */ 482 edit.HasPreferredX = false 483 return 1 484 } 485 486 return 0 487 } 488 489 // Paste from clipboard 490 func (edit *TextEditor) Paste(ctext string) { 491 if edit.Flags&EditReadOnly != 0 { 492 return 493 } 494 495 /* if there's a selection, the paste should delete it */ 496 edit.clamp() 497 498 edit.DeleteSelection() 499 500 text := []rune(ctext) 501 502 edit.Buffer = strInsertText(edit.Buffer, edit.Cursor, text) 503 504 edit.makeundoInsert(edit.Cursor, len(text)) 505 edit.Cursor += len(text) 506 edit.HasPreferredX = false 507 } 508 509 func (edit *TextEditor) Text(text []rune) { 510 if edit.Flags&EditReadOnly != 0 { 511 return 512 } 513 514 for i := range text { 515 /* can't add newline in single-line mode */ 516 if text[i] == '\n' && edit.SingleLine { 517 break 518 } 519 520 /* can't add tab in single-line mode */ 521 if text[i] == '\t' && edit.SingleLine { 522 break 523 } 524 525 /* filter incoming text */ 526 if edit.Filter != nil && !edit.Filter(text[i]) { 527 continue 528 } 529 530 if edit.InsertMode && !edit.hasSelection() && edit.Cursor < len(edit.Buffer) { 531 edit.makeundoReplace(edit.Cursor, 1, 1) 532 edit.Buffer = strDeleteText(edit.Buffer, edit.Cursor, 1) 533 edit.Buffer = strInsertText(edit.Buffer, edit.Cursor, text[i:i+1]) 534 edit.Cursor++ 535 edit.HasPreferredX = false 536 } else { 537 edit.DeleteSelection() /* implicitly clamps */ 538 edit.Buffer = strInsertText(edit.Buffer, edit.Cursor, text[i:i+1]) 539 edit.makeundoInsert(edit.Cursor, 1) 540 edit.Cursor++ 541 edit.HasPreferredX = false 542 } 543 } 544 } 545 546 func (state *TextEditor) key(e gkey.Event, font font.Face, row_height int, area_height int) { 547 readOnly := state.Flags&EditReadOnly != 0 548 retry: 549 switch e.Code { 550 case gkey.CodeZ: 551 if readOnly { 552 return 553 } 554 if e.Modifiers&gkey.ModCtrl != 0 { 555 if e.Modifiers&gkey.ModShift != 0 { 556 state.DoRedo() 557 state.HasPreferredX = false 558 559 } else { 560 state.DoUndo() 561 state.HasPreferredX = false 562 } 563 } 564 565 case gkey.CodeK: 566 if readOnly { 567 return 568 } 569 if e.Modifiers&gkey.ModCtrl != 0 { 570 state.trueSelectStart = state.Cursor 571 state.selectLine(state.Cursor) 572 state.DeleteSelection() 573 } 574 575 case gkey.CodeInsert: 576 state.InsertMode = !state.InsertMode 577 578 case gkey.CodeLeftArrow: 579 if e.Modifiers&gkey.ModCtrl != 0 { 580 if e.Modifiers&gkey.ModShift != 0 { 581 if !state.hasSelection() { 582 state.prepSelectionAtCursor() 583 } 584 state.Cursor = state.towd(state.Cursor-1, -1, false) 585 state.SelectEnd = state.Cursor 586 state.clamp() 587 } else { 588 if state.hasSelection() { 589 state.moveToFirst() 590 } else { 591 state.Cursor = state.towd(state.Cursor-1, -1, false) 592 state.clamp() 593 } 594 } 595 } else { 596 if e.Modifiers&gkey.ModShift != 0 { 597 state.clamp() 598 state.prepSelectionAtCursor() 599 600 /* move selection left */ 601 if state.SelectEnd > 0 { 602 state.SelectEnd-- 603 } 604 state.Cursor = state.SelectEnd 605 state.HasPreferredX = false 606 } else { 607 /* if currently there's a selection, 608 * move cursor to start of selection */ 609 if state.hasSelection() { 610 state.moveToFirst() 611 } else if state.Cursor > 0 { 612 state.Cursor-- 613 } 614 state.HasPreferredX = false 615 } 616 } 617 618 case gkey.CodeRightArrow: 619 if e.Modifiers&gkey.ModCtrl != 0 { 620 if e.Modifiers&gkey.ModShift != 0 { 621 if !state.hasSelection() { 622 state.prepSelectionAtCursor() 623 } 624 state.Cursor = state.towd(state.Cursor, +1, false) 625 state.SelectEnd = state.Cursor 626 state.clamp() 627 } else { 628 if state.hasSelection() { 629 state.moveToLast() 630 } else { 631 state.Cursor = state.towd(state.Cursor, +1, false) 632 state.clamp() 633 } 634 } 635 } else { 636 if e.Modifiers&gkey.ModShift != 0 { 637 state.prepSelectionAtCursor() 638 639 /* move selection right */ 640 state.SelectEnd++ 641 642 state.clamp() 643 state.Cursor = state.SelectEnd 644 state.HasPreferredX = false 645 } else { 646 /* if currently there's a selection, 647 * move cursor to end of selection */ 648 if state.hasSelection() { 649 state.moveToLast() 650 } else { 651 state.Cursor++ 652 } 653 state.clamp() 654 state.HasPreferredX = false 655 } 656 } 657 case gkey.CodeDownArrow: 658 if state.SingleLine { 659 e.Code = gkey.CodeRightArrow 660 goto retry 661 } 662 state.verticalCursorMove(e, font, row_height, +row_height) 663 664 case gkey.CodeUpArrow: 665 if state.SingleLine { 666 e.Code = gkey.CodeRightArrow 667 goto retry 668 } 669 state.verticalCursorMove(e, font, row_height, -row_height) 670 671 case gkey.CodePageDown: 672 if state.SingleLine { 673 break 674 } 675 state.verticalCursorMove(e, font, row_height, +area_height/2) 676 677 case gkey.CodePageUp: 678 if state.SingleLine { 679 break 680 } 681 state.verticalCursorMove(e, font, row_height, -area_height/2) 682 683 case gkey.CodeDeleteForward: 684 if readOnly { 685 return 686 } 687 if state.hasSelection() { 688 state.DeleteSelection() 689 } else { 690 if state.Cursor < len(state.Buffer) { 691 state.Delete(state.Cursor, 1) 692 } 693 } 694 695 state.HasPreferredX = false 696 697 case gkey.CodeDeleteBackspace: 698 if readOnly { 699 return 700 } 701 switch { 702 case state.hasSelection(): 703 state.DeleteSelection() 704 case e.Modifiers&gkey.ModCtrl != 0: 705 state.SelectEnd = state.Cursor 706 state.SelectStart = state.towd(state.Cursor-1, -1, false) 707 state.DeleteSelection() 708 default: 709 state.clamp() 710 if state.Cursor > 0 { 711 state.Delete(state.Cursor-1, 1) 712 state.Cursor-- 713 } 714 } 715 state.HasPreferredX = false 716 717 case gkey.CodeHome: 718 if e.Modifiers&gkey.ModCtrl != 0 { 719 if e.Modifiers&gkey.ModShift != 0 { 720 state.prepSelectionAtCursor() 721 state.SelectEnd = 0 722 state.Cursor = state.SelectEnd 723 state.HasPreferredX = false 724 } else { 725 state.SelectEnd = 0 726 state.SelectStart = state.SelectEnd 727 state.Cursor = state.SelectStart 728 state.HasPreferredX = false 729 } 730 } else { 731 state.clamp() 732 start := state.tonl(state.Cursor-1, -1) 733 if e.Modifiers&gkey.ModShift != 0 { 734 state.clamp() 735 state.prepSelectionAtCursor() 736 state.SelectEnd = start 737 state.Cursor = state.SelectEnd 738 state.HasPreferredX = false 739 } else { 740 state.clamp() 741 state.moveToFirst() 742 state.Cursor = start 743 state.HasPreferredX = false 744 } 745 } 746 747 case gkey.CodeA: 748 if e.Modifiers&gkey.ModCtrl != 0 { 749 state.clamp() 750 state.moveToFirst() 751 state.Cursor = state.tonl(state.Cursor-1, -1) 752 state.HasPreferredX = false 753 } 754 755 case gkey.CodeEnd: 756 if e.Modifiers&gkey.ModCtrl != 0 { 757 if e.Modifiers&gkey.ModShift != 0 { 758 state.prepSelectionAtCursor() 759 state.SelectEnd = len(state.Buffer) 760 state.Cursor = state.SelectEnd 761 state.HasPreferredX = false 762 } else { 763 state.Cursor = len(state.Buffer) 764 state.SelectEnd = 0 765 state.SelectStart = state.SelectEnd 766 state.HasPreferredX = false 767 } 768 } else { 769 state.clamp() 770 end := state.tonl(state.Cursor, +1) 771 if e.Modifiers&gkey.ModShift != 0 { 772 state.clamp() 773 state.prepSelectionAtCursor() 774 state.HasPreferredX = false 775 state.Cursor = end 776 state.SelectEnd = state.Cursor 777 } else { 778 state.clamp() 779 state.moveToFirst() 780 state.HasPreferredX = false 781 state.Cursor = end 782 } 783 } 784 785 case gkey.CodeE: 786 if e.Modifiers&gkey.ModCtrl != 0 { 787 end := state.tonl(state.Cursor, +1) 788 state.clamp() 789 state.moveToFirst() 790 state.HasPreferredX = false 791 state.Cursor = end 792 } 793 } 794 } 795 796 func (state *TextEditor) verticalCursorMove(e gkey.Event, font font.Face, row_height int, offset int) { 797 if e.Modifiers&gkey.ModShift != 0 { 798 state.prepSelectionAtCursor() 799 } else if state.hasSelection() { 800 if offset < 0 { 801 state.moveToFirst() 802 } else { 803 state.moveToLast() 804 } 805 } 806 807 state.clamp() 808 809 p := state.indexToCoord(state.Cursor, font, row_height) 810 p.Y += offset 811 812 if state.HasPreferredX { 813 p.X = state.PreferredX 814 } else { 815 state.HasPreferredX = true 816 state.PreferredX = p.X 817 } 818 state.Cursor = state.locateCoord(p, font, row_height) 819 820 state.clamp() 821 822 if e.Modifiers&gkey.ModShift != 0 { 823 state.SelectEnd = state.Cursor 824 } 825 } 826 827 func texteditFlushRedo(state *textUndoState) { 828 state.RedoPoint = int16(_TEXTEDIT_UNDOSTATECOUNT) 829 } 830 831 func texteditDiscardUndo(state *textUndoState) { 832 /* discard the oldest entry in the undo list */ 833 if state.UndoPoint > 0 { 834 state.UndoPoint-- 835 copy(state.UndoRec[:], state.UndoRec[1:]) 836 } 837 } 838 839 func texteditCreateUndoRecord(state *textUndoState, numchars int) *textUndoRecord { 840 /* any time we create a new undo record, we discard redo*/ 841 texteditFlushRedo(state) 842 843 /* if we have no free records, we have to make room, 844 * by sliding the existing records down */ 845 if int(state.UndoPoint) == _TEXTEDIT_UNDOSTATECOUNT { 846 texteditDiscardUndo(state) 847 } 848 849 r := &state.UndoRec[state.UndoPoint] 850 state.UndoPoint++ 851 return r 852 } 853 854 func texteditCreateundo(state *textUndoState, pos int, insert_len int, delete_len int) *textUndoRecord { 855 r := texteditCreateUndoRecord(state, insert_len) 856 857 r.Where = pos 858 r.InsertLength = insert_len 859 r.DeleteLength = delete_len 860 r.Text = nil 861 862 return r 863 } 864 865 func (edit *TextEditor) DoUndo() { 866 var s *textUndoState = &edit.Undo 867 var u textUndoRecord 868 var r *textUndoRecord 869 if s.UndoPoint == 0 { 870 return 871 } 872 873 /* we need to do two things: apply the undo record, and create a redo record */ 874 u = s.UndoRec[s.UndoPoint-1] 875 876 r = &s.UndoRec[s.RedoPoint-1] 877 r.Text = nil 878 879 r.InsertLength = u.DeleteLength 880 r.DeleteLength = u.InsertLength 881 r.Where = u.Where 882 883 if u.DeleteLength != 0 { 884 r.Text = make([]rune, u.DeleteLength) 885 copy(r.Text, edit.Buffer[u.Where:u.Where+u.DeleteLength]) 886 edit.Buffer = strDeleteText(edit.Buffer, u.Where, u.DeleteLength) 887 } 888 889 /* check type of recorded action: */ 890 if u.InsertLength != 0 { 891 /* easy case: was a deletion, so we need to insert n characters */ 892 edit.Buffer = strInsertText(edit.Buffer, u.Where, u.Text) 893 } 894 895 edit.Cursor = u.Where + u.InsertLength 896 897 s.UndoPoint-- 898 s.RedoPoint-- 899 } 900 901 func (edit *TextEditor) DoRedo() { 902 var s *textUndoState = &edit.Undo 903 var u *textUndoRecord 904 var r textUndoRecord 905 if int(s.RedoPoint) == _TEXTEDIT_UNDOSTATECOUNT { 906 return 907 } 908 909 /* we need to do two things: apply the redo record, and create an undo record */ 910 u = &s.UndoRec[s.UndoPoint] 911 912 r = s.UndoRec[s.RedoPoint] 913 914 /* we KNOW there must be room for the undo record, because the redo record 915 was derived from an undo record */ 916 u.DeleteLength = r.InsertLength 917 918 u.InsertLength = r.DeleteLength 919 u.Where = r.Where 920 u.Text = nil 921 922 if r.DeleteLength != 0 { 923 u.Text = make([]rune, r.DeleteLength) 924 copy(u.Text, edit.Buffer[r.Where:r.Where+r.DeleteLength]) 925 edit.Buffer = strDeleteText(edit.Buffer, r.Where, r.DeleteLength) 926 } 927 928 if r.InsertLength != 0 { 929 /* easy case: need to insert n characters */ 930 edit.Buffer = strInsertText(edit.Buffer, r.Where, r.Text) 931 } 932 933 edit.Cursor = r.Where + r.InsertLength 934 935 s.UndoPoint++ 936 s.RedoPoint++ 937 } 938 939 func (state *TextEditor) makeundoInsert(where int, length int) { 940 texteditCreateundo(&state.Undo, where, 0, length) 941 } 942 943 func (state *TextEditor) makeundoDelete(where int, length int) { 944 u := texteditCreateundo(&state.Undo, where, length, 0) 945 u.Text = make([]rune, length) 946 copy(u.Text, state.Buffer[where:where+length]) 947 } 948 949 func (state *TextEditor) makeundoReplace(where int, old_length int, new_length int) { 950 u := texteditCreateundo(&state.Undo, where, old_length, new_length) 951 u.Text = make([]rune, old_length) 952 copy(u.Text, state.Buffer[where:where+old_length]) 953 } 954 955 func (state *TextEditor) clearState(type_ TextEditType) { 956 /* reset the state to default */ 957 state.Undo.UndoPoint = 0 958 959 state.Undo.RedoPoint = int16(_TEXTEDIT_UNDOSTATECOUNT) 960 state.HasPreferredX = false 961 state.PreferredX = 0 962 //state.CursorAtEndOfLine = 0 963 state.Initialized = true 964 state.SingleLine = type_ == TextEditSingleLine 965 state.InsertMode = false 966 } 967 968 func (edit *TextEditor) SelectAll() { 969 edit.SelectStart = 0 970 edit.SelectEnd = len(edit.Buffer) 971 } 972 973 func (edit *TextEditor) editDrawText(out *command.Buffer, style *nstyle.Edit, pos image.Point, x_margin int, text []rune, textOffset int, row_height int, f font.Face, background color.RGBA, foreground color.RGBA, is_selected bool) (posOut image.Point) { 974 if len(text) == 0 { 975 return pos 976 } 977 var line_offset int = 0 978 var line_count int = 0 979 var txt textWidget 980 txt.Background = background 981 txt.Text = foreground 982 983 pos_x, pos_y := pos.X, pos.Y 984 start := 0 985 986 tabsz := glyphAdvance(f, ' ') * tabSizeInSpaces 987 pwsz := glyphAdvance(f, '*') 988 989 measureText := func(start, end int) int { 990 if edit.PasswordChar != 0 { 991 return pwsz * (end - start) 992 } 993 // XXX calculating text width here is slow figure out why 994 return measureRunes(f, text[start:end]) 995 } 996 997 getText := func(start, end int) string { 998 if edit.PasswordChar != 0 { 999 n := end - start 1000 if n >= len(edit.password) { 1001 edit.password = make([]rune, n) 1002 for i := range edit.password { 1003 edit.password[i] = edit.PasswordChar 1004 } 1005 } 1006 return string(edit.password[:n]) 1007 } 1008 return string(text[start:end]) 1009 } 1010 1011 flushLine := func(index int) rect.Rect { 1012 // new line sepeator so draw previous line 1013 var lblrect rect.Rect 1014 lblrect.Y = pos_y + line_offset 1015 lblrect.H = row_height 1016 lblrect.W = nk_null_rect.W 1017 lblrect.X = pos_x 1018 1019 if is_selected { // selection needs to draw different background color 1020 if index == len(text) || (index == start && start == 0) { 1021 lblrect.W = measureText(start, index) 1022 } 1023 out.FillRect(lblrect, 0, background) 1024 } 1025 edit.drawchunks = append(edit.drawchunks, drawchunk{lblrect, start + textOffset, index + textOffset}) 1026 widgetText(out, lblrect, getText(start, index), &txt, "LC", f) 1027 1028 pos_x = x_margin 1029 1030 return lblrect 1031 } 1032 1033 flushTab := func(index int) rect.Rect { 1034 var lblrect rect.Rect 1035 lblrect.Y = pos_y + line_offset 1036 lblrect.H = row_height 1037 lblrect.W = measureText(start, index) 1038 lblrect.X = pos_x 1039 1040 lblrect.W = int(math.Floor(float64(lblrect.X+lblrect.W-x_margin)/float64(tabsz))+1)*tabsz + x_margin - lblrect.X 1041 1042 if is_selected { 1043 out.FillRect(lblrect, 0, background) 1044 } 1045 edit.drawchunks = append(edit.drawchunks, drawchunk{lblrect, start + textOffset, index + textOffset}) 1046 widgetText(out, lblrect, getText(start, index), &txt, "LC", f) 1047 1048 pos_x += lblrect.W 1049 1050 return lblrect 1051 } 1052 1053 for index, glyph := range text { 1054 switch glyph { 1055 case '\t': 1056 flushTab(index) 1057 start = index + 1 1058 case '\n': 1059 flushLine(index) 1060 line_count++ 1061 start = index + 1 1062 line_offset += row_height 1063 1064 case '\r': 1065 // do nothing 1066 } 1067 } 1068 1069 if start >= len(text) { 1070 return image.Point{pos_x, pos_y + line_offset} 1071 } 1072 1073 // draw last line 1074 lblrect := flushLine(len(text)) 1075 lblrect.W = measureText(start, len(text)) 1076 1077 return image.Point{lblrect.X + lblrect.W, lblrect.Y} 1078 } 1079 1080 func (ed *TextEditor) doEdit(bounds rect.Rect, style *nstyle.Edit, inp *Input, cut, copy, paste bool) (ret EditEvents) { 1081 font := ed.win.ctx.Style.Font 1082 state := ed.win.widgets.PrevState(bounds) 1083 1084 ed.clamp() 1085 1086 // visible text area calculation 1087 var area rect.Rect 1088 area.X = bounds.X + style.Padding.X + style.Border 1089 area.Y = bounds.Y + style.Padding.Y + style.Border 1090 area.W = bounds.W - (2.0*style.Padding.X + 2*style.Border) 1091 area.H = bounds.H - (2.0*style.Padding.Y + 2*style.Border) 1092 if ed.Flags&EditMultiline != 0 { 1093 area.H = area.H - style.ScrollbarSize.Y 1094 } 1095 var row_height int 1096 if ed.Flags&EditMultiline != 0 { 1097 row_height = FontHeight(font) + style.RowPadding 1098 } else { 1099 row_height = area.H 1100 } 1101 1102 /* update edit state */ 1103 prev_state := ed.Active 1104 1105 if ed.win.ctx.activateEditor != nil { 1106 if ed.win.ctx.activateEditor == ed { 1107 ed.Active = true 1108 if ed.win.flags&windowDocked != 0 { 1109 ed.win.ctx.dockedWindowFocus = ed.win.idx 1110 } 1111 } else { 1112 ed.Active = false 1113 } 1114 } 1115 1116 is_hovered := inp.Mouse.HoveringRect(bounds) 1117 1118 if ed.Flags&EditFocusFollowsMouse != 0 { 1119 if inp != nil { 1120 ed.Active = is_hovered 1121 } 1122 } else { 1123 if inp != nil && inp.Mouse.Buttons[pointer.ButtonLeft].Clicked && inp.Mouse.Buttons[pointer.ButtonLeft].Down { 1124 ed.Active = inp.Mouse.HoveringRect(bounds) 1125 } 1126 } 1127 1128 /* (de)activate text editor */ 1129 var select_all bool 1130 if !prev_state && ed.Active { 1131 type_ := TextEditSingleLine 1132 if ed.Flags&EditMultiline != 0 { 1133 type_ = TextEditMultiLine 1134 } 1135 ed.clearState(type_) 1136 if ed.Flags&EditAlwaysInsertMode != 0 { 1137 ed.InsertMode = true 1138 } 1139 if ed.Flags&EditAutoSelect != 0 { 1140 select_all = true 1141 } 1142 } else if !ed.Active { 1143 ed.InsertMode = false 1144 } 1145 1146 if ed.Flags&EditNeverInsertMode != 0 { 1147 ed.InsertMode = false 1148 } 1149 1150 if ed.Active { 1151 ret = EditActive 1152 } else { 1153 ret = EditInactive 1154 } 1155 if prev_state != ed.Active { 1156 if ed.Active { 1157 ret |= EditActivated 1158 } else { 1159 ret |= EditDeactivated 1160 } 1161 } 1162 1163 /* handle user input */ 1164 cursor_follow := ed.CursorFollow 1165 ed.CursorFollow = false 1166 if ed.Active && inp != nil { 1167 inpos := inp.Mouse.Pos 1168 indelta := inp.Mouse.Delta 1169 coord := image.Point{(inpos.X - area.X), (inpos.Y - area.Y)} 1170 1171 var isHovered bool 1172 { 1173 areaWithoutScrollbar := area 1174 areaWithoutScrollbar.W -= style.ScrollbarSize.X 1175 isHovered = inp.Mouse.HoveringRect(areaWithoutScrollbar) 1176 } 1177 1178 var autoscrollTop bool 1179 { 1180 a := area 1181 a.W -= style.ScrollbarSize.X 1182 a.H = FontHeight(font) / 2 1183 autoscrollTop = inp.Mouse.HoveringRect(a) && inp.Mouse.Buttons[pointer.ButtonLeft].Down 1184 } 1185 1186 var autoscrollBot bool 1187 { 1188 a := area 1189 a.W -= style.ScrollbarSize.X 1190 a.Y = a.Y + a.H - FontHeight(font)/2 1191 a.H = FontHeight(font) / 2 1192 autoscrollBot = inp.Mouse.HoveringRect(a) && inp.Mouse.Buttons[pointer.ButtonLeft].Down 1193 } 1194 1195 /* mouse click handler */ 1196 if select_all { 1197 ed.SelectAll() 1198 } else if isHovered && inp.Mouse.Buttons[pointer.ButtonLeft].Down && inp.Mouse.Buttons[pointer.ButtonLeft].Clicked { 1199 if ed.doubleClick(coord) { 1200 ed.clickCount++ 1201 if ed.clickCount > 3 { 1202 ed.clickCount = 3 1203 } 1204 } else { 1205 ed.clickCount = 1 1206 } 1207 ed.click(coord, font, row_height) 1208 } else if isHovered && inp.Mouse.Buttons[pointer.ButtonLeft].Down && (indelta.X != 0.0 || indelta.Y != 0.0) { 1209 ed.drag(coord, font, row_height) 1210 cursor_follow = true 1211 } else if autoscrollTop { 1212 coord1 := coord 1213 coord1.Y -= FontHeight(font) 1214 ed.drag(coord1, font, row_height) 1215 cursor_follow = true 1216 } else if autoscrollBot { 1217 coord1 := coord 1218 coord1.Y += FontHeight(font) 1219 ed.drag(coord1, font, row_height) 1220 cursor_follow = true 1221 } 1222 1223 /* text input */ 1224 if inp.Keyboard.Text != "" { 1225 ed.Text([]rune(inp.Keyboard.Text)) 1226 cursor_follow = true 1227 } 1228 1229 clipboardModifier := gkey.ModCtrl 1230 if runtime.GOOS == "darwin" { 1231 clipboardModifier = gkey.ModMeta 1232 } 1233 1234 for _, e := range inp.Keyboard.Keys { 1235 switch e.Code { 1236 case gkey.CodeReturnEnter: 1237 if ed.Flags&EditCtrlEnterNewline != 0 && e.Modifiers&gkey.ModShift != 0 { 1238 ed.Text([]rune{'\n'}) 1239 cursor_follow = true 1240 } else if ed.Flags&EditSigEnter != 0 { 1241 ret = EditInactive 1242 ret |= EditDeactivated 1243 if ed.Flags&EditReadOnly == 0 { 1244 ret |= EditCommitted 1245 } 1246 ed.Active = false 1247 } 1248 1249 case gkey.CodeX: 1250 if e.Modifiers&clipboardModifier != 0 { 1251 cut = true 1252 } 1253 1254 case gkey.CodeC: 1255 if e.Modifiers&clipboardModifier != 0 { 1256 copy = true 1257 } 1258 1259 case gkey.CodeV: 1260 if e.Modifiers&clipboardModifier != 0 { 1261 paste = true 1262 } 1263 1264 case gkey.CodeF: 1265 if e.Modifiers&clipboardModifier != 0 { 1266 ed.popupFind() 1267 } 1268 1269 case gkey.CodeG: 1270 if e.Modifiers&clipboardModifier != 0 { 1271 ed.lookForward(true) 1272 cursor_follow = true 1273 } 1274 1275 default: 1276 ed.key(e, font, row_height, area.H) 1277 cursor_follow = true 1278 } 1279 1280 } 1281 1282 /* cut & copy handler */ 1283 if (copy || cut) && (ed.Flags&EditClipboard != 0) { 1284 var begin, end int 1285 if ed.SelectStart > ed.SelectEnd { 1286 begin = ed.SelectEnd 1287 end = ed.SelectStart 1288 } else { 1289 begin = ed.SelectStart 1290 end = ed.SelectEnd 1291 } 1292 clipboard.Set(string(ed.Buffer[begin:end])) 1293 if cut { 1294 ed.Cut() 1295 cursor_follow = true 1296 } 1297 } 1298 1299 /* paste handler */ 1300 if paste && (ed.Flags&EditClipboard != 0) { 1301 ed.Paste(clipboard.Get()) 1302 cursor_follow = true 1303 } 1304 1305 } 1306 1307 /* set widget state */ 1308 if ed.Active { 1309 state = nstyle.WidgetStateActive 1310 } else { 1311 state = nstyle.WidgetStateInactive 1312 } 1313 if is_hovered { 1314 state |= nstyle.WidgetStateHovered 1315 } 1316 1317 var d drawableTextEditor 1318 1319 /* text pointer positions */ 1320 var selection_begin, selection_end int 1321 if ed.SelectStart < ed.SelectEnd { 1322 selection_begin = ed.SelectStart 1323 selection_end = ed.SelectEnd 1324 } else { 1325 selection_begin = ed.SelectEnd 1326 selection_end = ed.SelectStart 1327 } 1328 1329 d.SelectionBegin, d.SelectionEnd = selection_begin, selection_end 1330 1331 d.Edit = ed 1332 d.State = state 1333 d.Style = style 1334 d.Scaling = ed.win.ctx.Style.Scaling 1335 d.Bounds = bounds 1336 d.Area = area 1337 d.RowHeight = row_height 1338 d.hasInput = inp.Mouse.valid 1339 ed.win.widgets.Add(state, bounds) 1340 d.Draw(&ed.win.ctx.Style, &ed.win.cmds) 1341 1342 /* scrollbar */ 1343 if cursor_follow { 1344 cursor_pos := d.CursorPos 1345 /* update scrollbar to follow cursor */ 1346 if ed.Flags&EditNoHorizontalScroll == 0 { 1347 /* horizontal scroll */ 1348 scroll_increment := area.W / 2 1349 if (cursor_pos.X < ed.Scrollbar.X) || ((ed.Scrollbar.X+area.W)-cursor_pos.X < FontWidth(font, "i")) { 1350 ed.Scrollbar.X = max(0, cursor_pos.X-scroll_increment) 1351 } 1352 } else { 1353 ed.Scrollbar.X = 0 1354 } 1355 1356 if ed.Flags&EditMultiline != 0 { 1357 /* vertical scroll */ 1358 if cursor_pos.Y < ed.Scrollbar.Y { 1359 ed.Scrollbar.Y = max(0, cursor_pos.Y-row_height) 1360 } 1361 for (ed.Scrollbar.Y+area.H)-cursor_pos.Y < row_height { 1362 ed.Scrollbar.Y = ed.Scrollbar.Y + row_height 1363 } 1364 } else { 1365 ed.Scrollbar.Y = 0 1366 } 1367 } 1368 1369 if !ed.SingleLine { 1370 /* scrollbar widget */ 1371 var scroll rect.Rect 1372 scroll.X = (area.X + area.W) - style.ScrollbarSize.X 1373 scroll.Y = area.Y 1374 scroll.W = style.ScrollbarSize.X 1375 scroll.H = area.H 1376 1377 scroll_offset := float64(ed.Scrollbar.Y) 1378 scroll_step := float64(scroll.H) * 0.1 1379 scroll_inc := float64(scroll.H) * 0.01 1380 scroll_target := float64(d.TextSize.Y + row_height) 1381 ed.Scrollbar.Y = int(doScrollbarv(ed.win, scroll, bounds, scroll_offset, scroll_target, scroll_step, scroll_inc, &style.Scrollbar, inp, font)) 1382 } 1383 1384 return ret 1385 } 1386 1387 type drawableTextEditor struct { 1388 Edit *TextEditor 1389 State nstyle.WidgetStates 1390 Style *nstyle.Edit 1391 Scaling float64 1392 Bounds rect.Rect 1393 Area rect.Rect 1394 RowHeight int 1395 hasInput bool 1396 1397 SelectionBegin, SelectionEnd int 1398 1399 TextSize image.Point 1400 CursorPos image.Point 1401 } 1402 1403 func (d *drawableTextEditor) Draw(z *nstyle.Style, out *command.Buffer) { 1404 edit := d.Edit 1405 state := d.State 1406 style := d.Style 1407 bounds := d.Bounds 1408 font := z.Font 1409 area := d.Area 1410 row_height := d.RowHeight 1411 selection_begin := d.SelectionBegin 1412 selection_end := d.SelectionEnd 1413 if edit.drawchunks != nil { 1414 edit.drawchunks = edit.drawchunks[:0] 1415 } 1416 1417 /* select background colors/images */ 1418 var old_clip rect.Rect = out.Clip 1419 { 1420 var background *nstyle.Item 1421 if state&nstyle.WidgetStateActive != 0 { 1422 background = &style.Active 1423 } else if state&nstyle.WidgetStateHovered != 0 { 1424 background = &style.Hover 1425 } else { 1426 background = &style.Normal 1427 } 1428 1429 /* draw background frame */ 1430 if background.Type == nstyle.ItemColor { 1431 out.FillRect(bounds, style.Rounding, style.BorderColor) 1432 out.FillRect(shrinkRect(bounds, style.Border), style.Rounding, background.Data.Color) 1433 } else { 1434 out.DrawImage(bounds, background.Data.Image) 1435 } 1436 } 1437 1438 area.W -= FontWidth(font, "i") 1439 clip := unify(old_clip, area) 1440 out.PushScissor(clip) 1441 /* draw text */ 1442 var background_color color.RGBA 1443 var text_color color.RGBA 1444 var sel_background_color color.RGBA 1445 var sel_text_color color.RGBA 1446 var cursor_color color.RGBA 1447 var cursor_text_color color.RGBA 1448 var background *nstyle.Item 1449 1450 /* select correct colors to draw */ 1451 if state&nstyle.WidgetStateActive != 0 { 1452 background = &style.Active 1453 text_color = style.TextActive 1454 sel_text_color = style.SelectedTextHover 1455 sel_background_color = style.SelectedHover 1456 cursor_color = style.CursorHover 1457 cursor_text_color = style.CursorTextHover 1458 } else if state&nstyle.WidgetStateHovered != 0 { 1459 background = &style.Hover 1460 text_color = style.TextHover 1461 sel_text_color = style.SelectedTextHover 1462 sel_background_color = style.SelectedHover 1463 cursor_text_color = style.CursorTextHover 1464 cursor_color = style.CursorHover 1465 } else { 1466 background = &style.Normal 1467 text_color = style.TextNormal 1468 sel_text_color = style.SelectedTextNormal 1469 sel_background_color = style.SelectedNormal 1470 cursor_color = style.CursorNormal 1471 cursor_text_color = style.CursorTextNormal 1472 } 1473 1474 if background.Type == nstyle.ItemImage { 1475 background_color = color.RGBA{0, 0, 0, 0} 1476 } else { 1477 background_color = background.Data.Color 1478 } 1479 1480 startPos := image.Point{area.X - edit.Scrollbar.X, area.Y - edit.Scrollbar.Y} 1481 pos := startPos 1482 x_margin := pos.X 1483 if edit.SelectStart == edit.SelectEnd { 1484 drawEolCursor := func() { 1485 cursor_pos := d.CursorPos 1486 /* draw cursor at end of line */ 1487 var cursor rect.Rect 1488 if edit.Flags&EditIbeamCursor != 0 { 1489 cursor.W = int(d.Scaling) 1490 if cursor.W <= 0 { 1491 cursor.W = 1 1492 } 1493 } else { 1494 cursor.W = FontWidth(font, "i") 1495 } 1496 cursor.H = row_height 1497 cursor.X = area.X + cursor_pos.X - edit.Scrollbar.X 1498 cursor.Y = area.Y + cursor_pos.Y + row_height/2.0 - cursor.H/2.0 1499 cursor.Y -= edit.Scrollbar.Y 1500 out.FillRect(cursor, 0, cursor_color) 1501 } 1502 1503 /* no selection so just draw the complete text */ 1504 pos = edit.editDrawText(out, style, pos, x_margin, edit.Buffer[:edit.Cursor], 0, row_height, font, background_color, text_color, false) 1505 d.CursorPos = pos.Sub(startPos) 1506 if edit.Active && d.hasInput { 1507 if edit.Cursor < len(edit.Buffer) { 1508 cursorChar := edit.Buffer[edit.Cursor] 1509 if cursorChar == '\n' || cursorChar == '\t' || edit.Flags&EditIbeamCursor != 0 { 1510 pos = edit.editDrawText(out, style, pos, x_margin, edit.Buffer[edit.Cursor:edit.Cursor+1], edit.Cursor, row_height, font, background_color, text_color, true) 1511 drawEolCursor() 1512 } else { 1513 pos = edit.editDrawText(out, style, pos, x_margin, edit.Buffer[edit.Cursor:edit.Cursor+1], edit.Cursor, row_height, font, cursor_color, cursor_text_color, true) 1514 } 1515 pos = edit.editDrawText(out, style, pos, x_margin, edit.Buffer[edit.Cursor+1:], edit.Cursor+1, row_height, font, background_color, text_color, false) 1516 } else { 1517 drawEolCursor() 1518 } 1519 } else if edit.Cursor < len(edit.Buffer) { 1520 pos = edit.editDrawText(out, style, pos, x_margin, edit.Buffer[edit.Cursor:], edit.Cursor, row_height, font, background_color, text_color, false) 1521 } 1522 } else { 1523 /* edit has selection so draw 1-3 text chunks */ 1524 if selection_begin > 0 { 1525 /* draw unselected text before selection */ 1526 pos = edit.editDrawText(out, style, pos, x_margin, edit.Buffer[:selection_begin], 0, row_height, font, background_color, text_color, false) 1527 } 1528 1529 if selection_begin == edit.SelectEnd { 1530 d.CursorPos = pos.Sub(startPos) 1531 } 1532 1533 pos = edit.editDrawText(out, style, pos, x_margin, edit.Buffer[selection_begin:selection_end], selection_begin, row_height, font, sel_background_color, sel_text_color, true) 1534 1535 if selection_begin != edit.SelectEnd { 1536 d.CursorPos = pos.Sub(startPos) 1537 } 1538 1539 if selection_end < len(edit.Buffer) { 1540 pos = edit.editDrawText(out, style, pos, x_margin, edit.Buffer[selection_end:], selection_end, row_height, font, background_color, text_color, false) 1541 } 1542 } 1543 d.TextSize = pos.Sub(startPos) 1544 1545 // fix rectangles in drawchunks by subtracting area from them 1546 for i := range edit.drawchunks { 1547 edit.drawchunks[i].X -= area.X 1548 edit.drawchunks[i].Y -= area.Y 1549 } 1550 1551 out.PushScissor(old_clip) 1552 } 1553 1554 func runeSliceEquals(a, b []rune) bool { 1555 if len(a) != len(b) { 1556 return false 1557 } 1558 for i := range a { 1559 if a[i] != b[i] { 1560 return false 1561 } 1562 } 1563 return true 1564 } 1565 1566 var clipboardModifier = func() gkey.Modifiers { 1567 if runtime.GOOS == "darwin" { 1568 return gkey.ModMeta 1569 } 1570 return gkey.ModCtrl 1571 }() 1572 1573 func (edit *TextEditor) popupFind() { 1574 if edit.Flags&EditMultiline == 0 { 1575 return 1576 } 1577 var searchEd TextEditor 1578 searchEd.Flags = EditSigEnter | EditClipboard | EditSelectable 1579 searchEd.Buffer = append(searchEd.Buffer[:0], edit.needle...) 1580 searchEd.SelectStart = 0 1581 searchEd.SelectEnd = len(searchEd.Buffer) 1582 searchEd.Cursor = searchEd.SelectEnd 1583 searchEd.Active = true 1584 1585 edit.SelectEnd = edit.SelectStart 1586 edit.Cursor = edit.SelectStart 1587 1588 edit.win.Master().PopupOpen("Search...", WindowTitle|WindowNoScrollbar|WindowMovable|WindowBorder|WindowDynamic, rect.Rect{100, 100, 400, 500}, true, func(w *Window) { 1589 w.Row(30).Static() 1590 w.LayoutFitWidth(0, 30) 1591 w.Label("Search: ", "LC") 1592 w.LayoutSetWidth(150) 1593 ev := searchEd.Edit(w) 1594 if ev&EditCommitted != 0 { 1595 edit.Active = true 1596 w.Close() 1597 } 1598 w.LayoutSetWidth(100) 1599 if w.ButtonText("Done") { 1600 edit.Active = true 1601 w.Close() 1602 } 1603 kbd := &w.Input().Keyboard 1604 for _, k := range kbd.Keys { 1605 switch { 1606 case k.Modifiers == clipboardModifier && k.Code == gkey.CodeG: 1607 edit.lookForward(true) 1608 case k.Modifiers == 0 && k.Code == gkey.CodeEscape: 1609 edit.SelectEnd = edit.SelectStart 1610 edit.Cursor = edit.SelectStart 1611 edit.Active = true 1612 w.Close() 1613 } 1614 } 1615 if !runeSliceEquals(searchEd.Buffer, edit.needle) { 1616 edit.needle = append(edit.needle[:0], searchEd.Buffer...) 1617 edit.lookForward(false) 1618 } 1619 }) 1620 } 1621 1622 func (edit *TextEditor) lookForward(forceAdvance bool) { 1623 if edit.Flags&EditMultiline == 0 { 1624 return 1625 } 1626 if edit.hasSelection() { 1627 if forceAdvance { 1628 edit.SelectStart = edit.SelectEnd 1629 } else { 1630 edit.SelectEnd = edit.SelectStart 1631 } 1632 if edit.SelectEnd >= 0 { 1633 edit.Cursor = edit.SelectEnd 1634 } 1635 } 1636 for start := edit.Cursor; start < len(edit.Buffer); start++ { 1637 found := true 1638 for i := 0; i < len(edit.needle); i++ { 1639 if edit.needle[i] != edit.Buffer[start+i] { 1640 found = false 1641 break 1642 } 1643 } 1644 if found { 1645 edit.SelectStart = start 1646 edit.SelectEnd = start + len(edit.needle) 1647 edit.Cursor = edit.SelectEnd 1648 edit.CursorFollow = true 1649 return 1650 } 1651 } 1652 edit.SelectStart = 0 1653 edit.SelectEnd = 0 1654 edit.Cursor = 0 1655 edit.CursorFollow = true 1656 } 1657 1658 // Adds text editor edit to win. 1659 // Initial contents of the text editor will be set to text. If 1660 // alwaysSet is specified the contents of the editor will be reset 1661 // to text. 1662 func (edit *TextEditor) Edit(win *Window) EditEvents { 1663 edit.init(win) 1664 if edit.Maxlen > 0 { 1665 if len(edit.Buffer) > edit.Maxlen { 1666 edit.Buffer = edit.Buffer[:edit.Maxlen] 1667 } 1668 } 1669 1670 if edit.Flags&EditNoCursor != 0 { 1671 edit.Cursor = len(edit.Buffer) 1672 } 1673 if edit.Flags&EditSelectable == 0 { 1674 edit.SelectStart = edit.Cursor 1675 edit.SelectEnd = edit.Cursor 1676 } 1677 1678 var bounds rect.Rect 1679 1680 style := &edit.win.ctx.Style 1681 widget_state, bounds, _ := edit.win.widget() 1682 if !widget_state { 1683 return 0 1684 } 1685 in := edit.win.inputMaybe(widget_state) 1686 1687 var cut, copy, paste bool 1688 1689 if w := win.ContextualOpen(0, image.Point{}, bounds, nil); w != nil { 1690 w.Row(20).Dynamic(1) 1691 visible := false 1692 if edit.Flags&EditClipboard != 0 { 1693 visible = true 1694 if w.MenuItem(label.TA("Cut", "LC")) { 1695 cut = true 1696 } 1697 if w.MenuItem(label.TA("Copy", "LC")) { 1698 copy = true 1699 } 1700 if w.MenuItem(label.TA("Paste", "LC")) { 1701 paste = true 1702 } 1703 } 1704 if edit.Flags&EditMultiline != 0 { 1705 visible = true 1706 if w.MenuItem(label.TA("Find...", "LC")) { 1707 edit.popupFind() 1708 } 1709 } 1710 if !visible { 1711 w.Close() 1712 } 1713 } 1714 1715 ev := edit.doEdit(bounds, &style.Edit, in, cut, copy, paste) 1716 return ev 1717 }