github.com/gop9/olt@v0.0.0-20200202132135-d956aad50b08/framework/richtext/rtxt.go (about) 1 package richtext 2 3 import ( 4 "fmt" 5 "image" 6 "image/color" 7 "math/rand" 8 "sort" 9 "strings" 10 "time" 11 "unicode" 12 "unicode/utf8" 13 14 "github.com/gop9/olt/framework" 15 "github.com/gop9/olt/framework/font" 16 "golang.org/x/image/math/fixed" 17 ) 18 19 type RichText struct { 20 name string 21 chunks []chunk // the full text of this widget, divided into chunks as they were added by the caller 22 styleSels []styleSel // styling for the text in non-overlapping selections of increasing S 23 Sel Sel // selected text if this widget is selectable, cursor position will have S == E 24 Flags Flags 25 flags Flags // flags for this widget 26 SelFgColor color.RGBA // foreground color for selection, zero value specifies that it should be copied from the window style 27 SelColor color.RGBA // background color for selection, zero value specifies that it should be copied from the window style 28 29 txtColor, selFgColor, selColor color.RGBA // default foreground color and background selected color 30 31 face font.Face // default face 32 33 first bool 34 ctor Ctor 35 36 changed bool // must recalculate text advances 37 width int // currently flowed width 38 maxWidth int // maximum line width 39 adv []fixed.Int26_6 // advance for each rune in chunks 40 lines []line // line of text, possibly wrapped 41 42 down bool // a click/selection is in progress 43 isClick bool // the click/selection in progress could be a click (unless the mouse moves too much) 44 dragStart int32 // stat of drag 45 lastClickTime time.Time 46 clickCount int // number of consecutive clicks 47 48 followCursor bool // scroll to show cursor on screen 49 50 focused bool 51 } 52 53 type Sel struct { 54 S, E int32 55 } 56 57 func (sel Sel) contains(p int32) bool { 58 return p >= sel.S && p < sel.E 59 } 60 61 type Flags uint16 62 63 const ( 64 AutoWrap Flags = 1 << iota 65 Selectable 66 Clipboard 67 ShowTick 68 Keyboard 69 ) 70 71 type FaceFlags uint16 72 73 const ( 74 Underline FaceFlags = 1 << iota 75 Strikethrough 76 ) 77 78 type Align uint8 79 80 const ( 81 AlignLeftDumb Align = iota 82 AlignLeft 83 AlignRight 84 AlignCenter 85 AlignJustified 86 ) 87 88 type chunk struct { 89 b []byte 90 s string 91 } 92 93 func (chunk chunk) len() int32 { 94 if chunk.b != nil { 95 return int32(len(chunk.b)) 96 } 97 return int32(len(chunk.s)) 98 } 99 100 func (c chunk) sub(start, end int32) chunk { 101 if c.b != nil { 102 return chunk{b: c.b[start:end]} 103 } 104 return chunk{s: c.s[start:end]} 105 } 106 107 func (chunk chunk) isspacing() bool { 108 if chunk.b != nil { 109 return len(chunk.b) == 0 || (len(chunk.b) == 1 && (chunk.b[0] == '\t' || chunk.b[0] == ' ')) 110 } 111 return chunk.s == "\t" || chunk.s == " " || chunk.s == "" 112 } 113 114 func (chunk chunk) runeCount() int { 115 if chunk.b != nil { 116 return utf8.RuneCount(chunk.b) 117 } 118 return utf8.RuneCountInString(chunk.s) 119 } 120 121 func (chunk chunk) str() string { 122 if chunk.b != nil { 123 return string(chunk.b) 124 } 125 return chunk.s 126 } 127 128 type TextStyle struct { 129 Face font.Face 130 Flags FaceFlags 131 132 Color, SelFgColor, BgColor color.RGBA // foreground color, selected foreground color, background color 133 134 Tooltip func(*framework.Window) 135 TooltipWidth int 136 137 ContextMenu func(*framework.Window) 138 } 139 140 type styleSel struct { 141 Sel 142 TextStyle 143 144 align Align 145 paraColor color.RGBA 146 147 link func() 148 isLink bool 149 150 hoverColor color.RGBA // hover color for links 151 } 152 153 type line struct { 154 chunks []chunk // each chunk has a uniform style 155 w []int // width of each chunk 156 off []int32 // character offset of the start of each chunk 157 runeoff int // offset in number of runes of the first character in the line 158 h int // height of the line 159 asc int // ascent of the biggest font of the line 160 p image.Point // insertion point for the line 161 162 leftMargin int // displacement from the left of the first chunk, used to implement right and center alignment 163 } 164 165 func (ln line) width() int { 166 w := 0 167 for i := range ln.w { 168 w += ln.w[i] 169 } 170 return w 171 } 172 173 func (ln line) endoff() int32 { 174 if len(ln.chunks) == 0 { 175 return ln.off[0] 176 } 177 return ln.off[len(ln.chunks)-1] + ln.chunks[len(ln.chunks)-1].len() 178 } 179 180 func (ln line) sel() Sel { 181 if len(ln.chunks) == 0 { 182 return Sel{ln.off[0], ln.off[0] + 1} 183 } 184 return Sel{ln.off[0], ln.endoff()} 185 } 186 187 type Ctor struct { 188 rtxt *RichText 189 mode ctorMode 190 w *framework.Window 191 off int32 // offset for append mode 192 len int32 193 194 chunks []chunk 195 styleSels []styleSel // not ordered, possibly overlapping 196 alignSels []styleSel // not ordered, possibly overlapping 197 linkSels []styleSel 198 199 savedStyleOk bool 200 savedStyle styleSel 201 202 selsDone bool // all sels are closed 203 204 linkClick int32 // position of click in link mode 205 206 appendScroll bool 207 } 208 209 type ctorMode uint8 210 211 const ( 212 ctorWidget ctorMode = iota 213 ctorRows 214 ctorAppend 215 ctorLink 216 ) 217 218 func New(flags Flags) *RichText { 219 n := rand.Int() 220 return &RichText{ 221 name: fmt.Sprintf("richtext%d", n), 222 Flags: flags, 223 flags: flags, 224 first: true, 225 } 226 } 227 228 // Widget adds this rich text widget as a widget to w. The returned 229 // constructor allows populating the rich text widget with text and 230 // responding to events. 231 // The constructor will be returned under this circumstances: 232 // 1. the first time this function or Rows is called 233 // 2. changed is true 234 // 3. a link was clicked 235 func (rtxt *RichText) Widget(w *framework.Window, changed bool) *Ctor { 236 rtxt.initialize(w) 237 if rtxt.first || changed { 238 rtxt.changed = true 239 rtxt.ctor = Ctor{rtxt: rtxt, mode: ctorWidget, w: w} 240 return &rtxt.ctor 241 } 242 return rtxt.drawWidget(w) 243 } 244 245 // Rows is like Widget but adds the contents as a series of rows instead of 246 // a single widget. 247 func (rtxt *RichText) Rows(w *framework.Window, changed bool) *Ctor { 248 rtxt.initialize(w) 249 if rtxt.first || changed { 250 rtxt.changed = true 251 rtxt.ctor = Ctor{rtxt: rtxt, mode: ctorRows, w: w} 252 return &rtxt.ctor 253 } 254 return rtxt.drawRows(w, 0) 255 } 256 257 // Append allows adding text at the end of the widget. Calling Link or 258 // LinkBytes on the returned constructor is illegal if callback is nil. 259 // Selections for SetStyleForSel and SetAlignForSel are interpreted to be 260 // relative to the newly added text. 261 func (rtxt *RichText) Append(scroll bool) *Ctor { 262 if rtxt.first { 263 panic("Append called on a RichText widget that isn't initialized") 264 } 265 off := int32(0) 266 for _, chunk := range rtxt.chunks { 267 off += chunk.len() 268 } 269 rtxt.ctor = Ctor{rtxt: rtxt, mode: ctorAppend, off: off, appendScroll: scroll} 270 271 lastSel := rtxt.styleSels[len(rtxt.styleSels)-1] 272 273 rtxt.ctor.alignSels = append(rtxt.ctor.alignSels, styleSel{Sel: Sel{S: int32(rtxt.ctor.off + rtxt.ctor.len)}, align: lastSel.align, paraColor: lastSel.paraColor}) 274 return &rtxt.ctor 275 } 276 277 // Look searches from the next occurence of text inside rtxt, starting at 278 // rtxt.Sel.S. Restarts the search from the start if nothing is found. 279 func (rtxt *RichText) Look(text string, wraparound bool) bool { 280 insensitive := true 281 282 for _, ch := range text { 283 if unicode.IsUpper(ch) { 284 insensitive = false 285 break 286 } 287 } 288 289 var titer textIter 290 titer.Init(rtxt) 291 if titer.Advance(rtxt.Sel.S) { 292 for { 293 if ok, sel := textIterMatch(titer, text, insensitive); ok { 294 rtxt.Sel = sel 295 return true 296 } 297 if !titer.Next() { 298 break 299 } 300 } 301 } 302 303 if !wraparound { 304 return false 305 } 306 307 titer.Init(rtxt) 308 for { 309 if ok, sel := textIterMatch(titer, text, insensitive); ok { 310 rtxt.Sel = sel 311 return true 312 } 313 if !titer.Next() { 314 break 315 } 316 } 317 318 rtxt.Sel.S = rtxt.Sel.E 319 return false 320 } 321 322 func (rtxt *RichText) FollowCursor() { 323 rtxt.followCursor = true 324 } 325 326 func (rtxt *RichText) Get(sel Sel) string { 327 var titer textIter 328 titer.Init(rtxt) 329 if !titer.Advance(rtxt.Sel.S) { 330 return "" 331 } 332 333 startChunkIdx := titer.chunkIdx 334 startByteIdx := titer.j 335 336 var endChunkIdx int 337 var endByteIdx int32 338 if titer.Advance(rtxt.Sel.E - rtxt.Sel.S) { 339 endChunkIdx = titer.chunkIdx 340 endByteIdx = titer.j 341 } else { 342 endChunkIdx = len(rtxt.chunks) 343 endByteIdx = 0 344 } 345 346 var out strings.Builder 347 out.WriteString(rtxt.chunks[startChunkIdx].sub(startByteIdx, rtxt.chunks[startChunkIdx].len()).str()) 348 349 for i := startChunkIdx + 1; i < endChunkIdx; i++ { 350 out.WriteString(rtxt.chunks[i].str()) 351 } 352 353 if endChunkIdx < len(rtxt.chunks) { 354 out.WriteString(rtxt.chunks[endChunkIdx].sub(0, endByteIdx).str()) 355 } 356 357 return out.String() 358 } 359 360 // Tail removes everything but the last n physical lines 361 func (rtxt *RichText) Tail(n int) { 362 if len(rtxt.lines) <= n { 363 return 364 } 365 366 off := rtxt.lines[len(rtxt.lines)-n].off[0] 367 runeoff := rtxt.lines[len(rtxt.lines)-n].off[0] 368 369 off2 := int32(0) 370 for i, chunk := range rtxt.chunks { 371 if off2+chunk.len() > off { 372 rtxt.chunks = append(rtxt.chunks[:0], rtxt.chunks[i:]...) 373 s := off - off2 374 if s >= chunk.len() { 375 rtxt.chunks = append(rtxt.chunks[:0], rtxt.chunks[1:]...) 376 } else { 377 if rtxt.chunks[0].b != nil { 378 rtxt.chunks[0].b = rtxt.chunks[0].b[s:] 379 } else { 380 rtxt.chunks[0].s = rtxt.chunks[0].s[s:] 381 } 382 } 383 break 384 } 385 off2 += chunk.len() 386 } 387 388 rtxt.adv = append(rtxt.adv[:0], rtxt.adv[runeoff:]...) 389 rtxt.reflow() 390 391 } 392 393 func (rtxt *RichText) initialize(w *framework.Window) { 394 style := w.Master().Style() 395 if (rtxt.SelColor != color.RGBA{}) { 396 rtxt.selColor = rtxt.SelColor 397 } else { 398 rtxt.selColor = style.Edit.SelectedHover 399 } 400 if (rtxt.SelFgColor != color.RGBA{}) { 401 rtxt.selFgColor = rtxt.SelFgColor 402 } else { 403 rtxt.selFgColor = style.Edit.SelectedTextHover 404 } 405 rtxt.txtColor = style.Edit.TextActive 406 if rtxt.face != style.Font { 407 rtxt.changed = true 408 } 409 rtxt.face = style.Font 410 if rtxt.flags != rtxt.Flags { 411 rtxt.flags = rtxt.Flags 412 rtxt.changed = true 413 } 414 } 415 416 func (ctor *Ctor) End() { 417 if !ctor.selsDone { 418 ctor.closeSels() 419 } 420 ctor.styleSels = removeEmptySels(ctor.styleSels) 421 ctor.alignSels = removeEmptySels(ctor.alignSels) 422 ctor.linkSels = removeEmptySels(ctor.linkSels) 423 ctor.styleSels = separateStyles(ctor.styleSels) 424 ctor.alignSels = separateStyles(ctor.alignSels) 425 styleSels := mergeStyles(ctor.styleSels, ctor.alignSels, ctor.linkSels) 426 styleSels = removeEmptySels(styleSels) 427 switch ctor.mode { 428 case ctorAppend: 429 n := len(ctor.rtxt.chunks) 430 ctor.rtxt.chunks = append(ctor.rtxt.chunks, ctor.chunks...) 431 ctor.rtxt.styleSels = append(ctor.rtxt.styleSels, styleSels...) 432 ctor.rtxt.calcAdvances(n) 433 ctor.rtxt.reflow() 434 if ctor.appendScroll { 435 ctor.rtxt.Sel = Sel{ctor.off + ctor.len, ctor.off + ctor.len} 436 ctor.rtxt.followCursor = true 437 } 438 case ctorRows: 439 ctor.rtxt.styleSels = styleSels 440 ctor.rtxt.chunks = ctor.chunks 441 ctor.rtxt.drawRows(ctor.w, 0) 442 case ctorWidget: 443 ctor.rtxt.styleSels = styleSels 444 ctor.rtxt.chunks = ctor.chunks 445 ctor.rtxt.drawWidget(ctor.w) 446 case ctorLink: 447 // nothing to do 448 } 449 } 450 451 // Align changes current text alignment. It is only legal to call Align 452 // before inserting text or after a newline character is inserted. 453 func (ctor *Ctor) Align(align Align) { 454 ctor.ParagraphStyle(align, color.RGBA{}) 455 } 456 457 func (ctor *Ctor) ParagraphStyle(align Align, color color.RGBA) { 458 const badAlign = "Align call not at the start of a line" 459 var lastChunk chunk 460 if len(ctor.chunks) > 0 { 461 lastChunk = ctor.chunks[len(ctor.chunks)-1] 462 } else if ctor.mode == ctorAppend && len(ctor.rtxt.chunks) > 0 { 463 lastChunk = ctor.rtxt.chunks[len(ctor.rtxt.chunks)-1] 464 } 465 if lastChunk.b != nil && lastChunk.b[len(lastChunk.b)-1] != '\n' { 466 panic(badAlign) 467 } else if len(lastChunk.s) > 0 && lastChunk.s[len(lastChunk.s)-1] != '\n' { 468 panic(badAlign) 469 } 470 471 if ctor.mode == ctorLink { 472 return 473 } 474 475 if len(ctor.alignSels) > 0 { 476 ctor.alignSels[len(ctor.alignSels)-1].E = int32(ctor.off + ctor.len) 477 } 478 ctor.alignSels = append(ctor.alignSels, styleSel{Sel: Sel{S: int32(ctor.off + ctor.len)}, align: align, paraColor: color}) 479 } 480 481 // SetStyle changes current text style. If color or selColor are the zero 482 // value the default value (copied from the window) will be used. 483 // If face is nil the default font face from the window style will be used. 484 func (ctor *Ctor) SetStyle(s TextStyle) { 485 if ctor.mode == ctorLink { 486 return 487 } 488 if len(ctor.styleSels) > 0 { 489 ctor.styleSels[len(ctor.styleSels)-1].E = ctor.off + ctor.len 490 } 491 ctor.styleSels = append(ctor.styleSels, styleSel{Sel: Sel{S: int32(ctor.off + ctor.len)}, TextStyle: s}) 492 } 493 494 // SaveStyle saves the current text style. 495 func (ctor *Ctor) SaveStyle() { 496 if ctor.mode == ctorLink { 497 return 498 } 499 if len(ctor.styleSels) <= 0 || ctor.styleSels[len(ctor.styleSels)-1].E != 0 { 500 ctor.savedStyleOk = false 501 } else { 502 ctor.savedStyle = ctor.styleSels[len(ctor.styleSels)-1] 503 ctor.savedStyleOk = true 504 } 505 } 506 507 // ClearStyle resets the text style to the default. 508 func (ctor *Ctor) ClearStyle() { 509 if len(ctor.styleSels) > 0 { 510 ctor.styleSels[len(ctor.styleSels)-1].E = ctor.off + ctor.len 511 } 512 } 513 514 // RestoreStyle restores the last saved text style. 515 func (ctor *Ctor) RestoreStyle() { 516 if ctor.mode == ctorLink { 517 return 518 } 519 if ctor.savedStyleOk { 520 ctor.SetStyle(ctor.savedStyle.TextStyle) 521 } else { 522 ctor.ClearStyle() 523 } 524 } 525 526 // SetAlignForSel changes the alignment for the specified region. 527 func (ctor *Ctor) SetAlignForSel(sel Sel, align Align) { 528 ctor.SetParagraphStyleForSel(sel, align, color.RGBA{}) 529 } 530 531 func (ctor *Ctor) SetParagraphStyleForSel(sel Sel, align Align, color color.RGBA) { 532 if !ctor.selsDone { 533 ctor.closeSels() 534 } 535 if ctor.mode == ctorLink { 536 return 537 } 538 const badAlign = "Align call not at the start of a line" 539 if sel.S > 0 { 540 if ctor.findByte(sel.S-1) != '\n' { 541 panic(badAlign) 542 } 543 } 544 if sel.E > 0 && sel.E < ctor.len { 545 if ctor.findByte(sel.E-1) != '\n' { 546 panic(badAlign) 547 } 548 } 549 ctor.alignSels = append(ctor.alignSels, styleSel{Sel: sel, align: align, paraColor: color}) 550 } 551 552 func (ctor *Ctor) findByte(off int32) byte { 553 cur := int32(0) 554 for _, chunk := range ctor.chunks { 555 if off < cur+chunk.len() { 556 chunkoff := off - cur 557 if chunk.b != nil { 558 return chunk.b[chunkoff] 559 } 560 return chunk.s[chunkoff] 561 } 562 cur += chunk.len() 563 } 564 return 0 565 } 566 567 // SetStyleForSel changes the text style for the specified region. 568 func (ctor *Ctor) SetStyleForSel(sel Sel, s TextStyle) { 569 if !ctor.selsDone { 570 ctor.closeSels() 571 } 572 if ctor.mode == ctorLink { 573 return 574 } 575 ctor.styleSels = append(ctor.styleSels, styleSel{Sel: sel, TextStyle: s}) 576 } 577 578 // Text adds text to the widget. 579 func (ctor *Ctor) Text(text string) { 580 ctor.textChunk(chunk{s: text}) 581 } 582 583 // TextBytes adds text to the widget. 584 //TODO: reenable when implemetned 585 // func (ctor *Ctor) TextBytes(text []byte) { 586 // ctor.textChunk(chunk{b: text}) 587 // } 588 589 func (ctor *Ctor) textChunk(chunk chunk) { 590 if ctor.selsDone { 591 panic("Text add after selections were finalized") 592 } 593 if chunk.len() <= 0 { 594 return 595 } 596 ctor.chunks = append(ctor.chunks, chunk) 597 ctor.len += chunk.len() 598 } 599 600 // Link adds a link to the widget. It will return true if the link was 601 // clicked and callback is nil. 602 func (ctor *Ctor) Link(text string, hoverColor color.RGBA, callback func()) bool { 603 return ctor.linkChunk(chunk{s: text}, hoverColor, callback) 604 } 605 606 // LinkBytes adds a link to the widget. It will return true if the link was 607 // clicked and callback is nil. 608 //TODO: reenable when implemented 609 // func (ctor *Ctor) LinkBytes(text []byte, callback func(string)) bool { 610 // panic("not implemented") 611 // return ctor.linkChunk(chunk{b: text}, callback) 612 // } 613 614 func (ctor *Ctor) linkChunk(chunk chunk, hoverColor color.RGBA, callback func()) bool { 615 if ctor.mode == ctorAppend && callback == nil { 616 panic("Link added in append mode without callback") 617 } 618 if ctor.selsDone { 619 panic("Link added after selections were finalized") 620 } 621 if chunk.len() <= 0 { 622 return false 623 } 624 sel := Sel{S: ctor.len + ctor.off} 625 ctor.textChunk(chunk) 626 sel.E = ctor.len + ctor.off 627 ctor.linkSels = append(ctor.linkSels, styleSel{Sel: sel, link: callback, isLink: true, hoverColor: hoverColor}) 628 return ctor.mode == ctorLink && sel.contains(ctor.linkClick) 629 } 630 631 func (ctor *Ctor) closeSels() { 632 ctor.selsDone = true 633 if len(ctor.alignSels) > 0 { 634 ctor.alignSels[len(ctor.alignSels)-1].E = int32(ctor.off + ctor.len) 635 } 636 if len(ctor.styleSels) > 0 { 637 ctor.styleSels[len(ctor.styleSels)-1].E = int32(ctor.off + ctor.len) 638 } 639 } 640 641 // separateStyles takes a slice of possibly overlapping styles and makes it 642 // non-overlapping. 643 func separateStyles(ssels []styleSel) []styleSel { 644 sort.SliceStable(ssels, func(i, j int) bool { return ssels[i].S < ssels[j].S }) 645 646 sstack := []styleSel{} 647 cur := int32(0) 648 r := make([]styleSel, 0, len(ssels)) 649 650 for len(ssels) > 0 || len(sstack) > 0 { 651 switch { 652 case len(sstack) == 0: 653 sstack = append(sstack, ssels[0]) 654 cur = ssels[0].S 655 ssels = ssels[1:] 656 657 case len(ssels) == 0 || sstack[len(sstack)-1].E < ssels[0].S: 658 s := sstack[len(sstack)-1] 659 if cur < s.Sel.E { 660 s.Sel.S = cur 661 r = append(r, s) 662 cur = s.Sel.E 663 } 664 sstack = sstack[:len(sstack)-1] 665 666 default: 667 s := sstack[len(sstack)-1] 668 s.Sel.S = cur 669 cur = ssels[0].S 670 s.Sel.E = cur 671 r = append(r, s) 672 sstack = append(sstack, ssels[0]) 673 ssels = ssels[1:] 674 } 675 } 676 677 return r 678 } 679 680 func mergeStyles(styleSels, alignSels, linkSels []styleSel) []styleSel { 681 minPosAfter := func(p0 int32) int32 { 682 min := int32((^uint32(0)) >> 1) 683 684 if len(styleSels) > 0 && styleSels[0].S > p0 && styleSels[0].S < min { 685 min = styleSels[0].S 686 } 687 if len(alignSels) > 0 && alignSels[0].S > p0 && alignSels[0].S < min { 688 min = alignSels[0].S 689 } 690 if len(linkSels) > 0 && linkSels[0].S > p0 && linkSels[0].S < min { 691 min = linkSels[0].S 692 } 693 if len(styleSels) > 0 && styleSels[0].E > p0 && styleSels[0].E < min { 694 min = styleSels[0].E 695 } 696 if len(alignSels) > 0 && alignSels[0].E > p0 && alignSels[0].E < min { 697 min = alignSels[0].E 698 } 699 if len(linkSels) > 0 && linkSels[0].E > p0 && linkSels[0].E < min { 700 min = linkSels[0].E 701 } 702 return min 703 } 704 705 r := make([]styleSel, 0, len(styleSels)) 706 707 appendCurrentStyle := func(sel Sel) { 708 s := styleSel{Sel: sel} 709 ok := false 710 711 if len(styleSels) > 0 && styleSels[0].contains(sel.S) { 712 s = styleSels[0] 713 s.Sel = sel 714 ok = true 715 } 716 if len(alignSels) > 0 && alignSels[0].contains(sel.S) { 717 s.align = alignSels[0].align 718 s.paraColor = alignSels[0].paraColor 719 ok = true 720 } 721 if len(linkSels) > 0 && linkSels[0].contains(sel.S) { 722 s.link = linkSels[0].link 723 s.isLink = linkSels[0].isLink 724 s.hoverColor = linkSels[0].hoverColor 725 ok = true 726 } 727 if ok { 728 r = append(r, s) 729 } 730 } 731 732 removeBefore := func(p0 int32) { 733 if len(styleSels) > 0 && styleSels[0].E <= p0 { 734 styleSels = styleSels[1:] 735 } 736 if len(alignSels) > 0 && alignSels[0].E <= p0 { 737 alignSels = alignSels[1:] 738 } 739 if len(linkSels) > 0 && linkSels[0].E <= p0 { 740 linkSels = linkSels[1:] 741 } 742 } 743 744 cur := minPosAfter(-1) 745 746 for { 747 next := minPosAfter(cur) 748 appendCurrentStyle(Sel{cur, next}) 749 cur = next 750 removeBefore(cur) 751 if len(styleSels) == 0 && len(alignSels) == 0 && len(linkSels) == 0 { 752 break 753 } 754 } 755 756 return r 757 } 758 759 func removeEmptySels(styleSels []styleSel) []styleSel { 760 r := styleSels[:0] 761 for _, styleSel := range styleSels { 762 if styleSel.S < styleSel.E { 763 r = append(r, styleSel) 764 } 765 } 766 return r 767 } 768 769 type textIter struct { 770 rtxt *RichText 771 chunkIdx int 772 j int32 773 off int32 774 valid bool 775 } 776 777 func (titer *textIter) Init(rtxt *RichText) { 778 titer.rtxt = rtxt 779 titer.chunkIdx = 0 780 titer.j = 0 781 titer.off = 0 782 titer.valid = true 783 } 784 785 func (titer *textIter) Next() bool { 786 if !titer.valid { 787 return false 788 } 789 titer.j++ 790 titer.off++ 791 792 if titer.j >= titer.rtxt.chunks[titer.chunkIdx].len() { 793 titer.j = 0 794 titer.chunkIdx++ 795 if titer.chunkIdx >= len(titer.rtxt.chunks) { 796 titer.valid = false 797 return false 798 } 799 } 800 801 return true 802 } 803 804 func (titer *textIter) Char() byte { 805 if !titer.valid { 806 return 0 807 } 808 chunk := titer.rtxt.chunks[titer.chunkIdx] 809 if chunk.b != nil { 810 return chunk.b[titer.j] 811 } 812 return chunk.s[titer.j] 813 } 814 815 func (titer *textIter) Advance(off int32) bool { 816 titer.j += off 817 titer.off += off 818 819 for titer.j >= titer.rtxt.chunks[titer.chunkIdx].len() { 820 titer.j -= titer.rtxt.chunks[titer.chunkIdx].len() 821 titer.chunkIdx++ 822 if titer.chunkIdx >= len(titer.rtxt.chunks) { 823 titer.valid = false 824 return false 825 } 826 } 827 828 return true 829 } 830 831 func (titer *textIter) Valid() bool { 832 return titer.valid 833 } 834 835 func textIterMatch(titer textIter, needle string, insensitive bool) (bool, Sel) { 836 start := titer.off 837 for i := 0; i < len(needle); i++ { 838 ch := titer.Char() 839 if insensitive { 840 ch = byte(unicode.ToLower(rune(ch))) 841 } 842 if !titer.Valid() || needle[i] != ch { 843 return false, Sel{} 844 } 845 titer.Next() 846 } 847 return true, Sel{start, titer.off} 848 } 849 850 func (rtxt *RichText) length() int32 { 851 r := int32(0) 852 for _, chunk := range rtxt.chunks { 853 r += chunk.len() 854 } 855 return r 856 }