github.com/gop9/olt@v0.0.0-20200202132135-d956aad50b08/framework/richtext/draw.go (about) 1 package richtext 2 3 import ( 4 "github.com/gop9/olt/gio/io/pointer" 5 "image" 6 "image/color" 7 "unicode/utf8" 8 9 "github.com/gop9/olt/framework" 10 "github.com/gop9/olt/framework/command" 11 "github.com/gop9/olt/framework/font" 12 "github.com/gop9/olt/framework/rect" 13 nstyle "github.com/gop9/olt/framework/style" 14 "golang.org/x/image/math/fixed" 15 ) 16 17 const debugDrawBoundingBoxes = false 18 19 func (rtxt *RichText) drawWidget(w *framework.Window) *Ctor { 20 rtxt.first = false 21 22 var flags framework.WindowFlags = 0 23 if rtxt.flags&AutoWrap != 0 { 24 flags = framework.WindowNoHScrollbar 25 } 26 27 wp := w 28 if w := w.GroupBegin(rtxt.name, flags); w != nil { 29 r := rtxt.drawRows(w, wp.LastWidgetBounds.H) 30 w.GroupEnd() 31 return r 32 } 33 34 return nil 35 } 36 37 func (rtxt *RichText) drawRows(w *framework.Window, viewporth int) *Ctor { 38 arrowKey, pageKey := 0, 0 39 if rtxt.focused && rtxt.flags&Keyboard != 0 { 40 arrowKey, pageKey = rtxt.handleKeyboard(w.Input()) 41 } 42 if viewporth == 0 { 43 viewporth = w.Bounds.H - w.At().Y 44 } 45 46 wasFocused := rtxt.focused 47 if rtxt.flags&Keyboard != 0 && (w.Input().Mouse.Buttons[pointer.ButtonLeft].Down || w.Input().Mouse.Buttons[pointer.ButtonRight].Down) { 48 rtxt.focused = false 49 } 50 51 rtxt.first = false 52 // this small row is necessary so that LayoutAvailableWidth will give us 53 // the correct available width for our shit. 54 if rtxt.flags&AutoWrap != 0 { 55 w.RowScaled(1).Dynamic(1) 56 } else { 57 w.RowScaled(1).StaticScaled(1) 58 } 59 width := w.LayoutAvailableWidth() 60 if rtxt.changed { 61 rtxt.calcAdvances(0) 62 } 63 if width != rtxt.width || rtxt.changed { 64 rtxt.width = width 65 if rtxt.changed || rtxt.flags&AutoWrap != 0 { 66 rtxt.reflow() 67 } 68 } 69 70 rtxt.changed = false 71 72 in := w.Input() 73 rowSpacing := w.WindowStyle().Spacing.Y 74 75 var siter styleIterator 76 siter.Init(rtxt) 77 78 const ( 79 selBefore = iota 80 selInside 81 selAfter 82 selTick 83 ) 84 85 insel := selBefore 86 87 linkClick := int32(-1) 88 scrollbary := w.Scrollbar.Y 89 90 for i := range rtxt.lines { 91 line := &rtxt.lines[i] 92 lineidx := i 93 if rtxt.flags&AutoWrap != 0 { 94 w.RowScaled(line.h).Dynamic(1) 95 } else { 96 w.RowScaled(line.h).StaticScaled(line.width()) 97 } 98 99 bounds, out := w.Custom(nstyle.WidgetStateActive) 100 if rtxt.followCursor && (line.sel().contains(rtxt.Sel.S) || line.endoff() == rtxt.Sel.S) { 101 rtxt.followCursor = false 102 r := bounds 103 r.Intersect(&w.Bounds) 104 if out == nil || r.H < line.h { 105 scrollbary = w.At().Y - w.Bounds.H/2 106 if scrollbary < 0 { 107 scrollbary = 0 108 } 109 } 110 } 111 if out == nil { 112 continue 113 } 114 if debugDrawBoundingBoxes { 115 out.FillRect(bounds, 0, color.RGBA{0, 0xff, 0, 0xff}) 116 r := bounds 117 r.W = rtxt.width 118 out.FillRect(r, 0, color.RGBA{0, 0, 0xff, 0xff}) 119 } 120 line.p = image.Point{bounds.X, bounds.Y} 121 p := line.p 122 123 if len(line.off) > 0 { 124 siter.AdvanceTo(line.off[0]) 125 } 126 127 if siter.styleSel.paraColor != (color.RGBA{}) { 128 r := bounds 129 r.W = rtxt.maxWidth 130 r.H += rowSpacing 131 out.FillRect(r, 0, siter.styleSel.paraColor) 132 } 133 134 if line.leftMargin > 0 { 135 // click before the first chunk of the line 136 rtxt.handleClick(w, rect.Rect{X: p.X, Y: p.Y, W: line.leftMargin, H: line.h + rowSpacing}, in, siter.styleSel, line, 0, nil, nil) 137 } 138 139 p.X += line.leftMargin 140 141 if rtxt.Sel.S == rtxt.Sel.E { 142 insel = selTick 143 } 144 145 for i, chunk := range line.chunks { 146 siter.AdvanceTo(line.off[i]) 147 148 hovering := false 149 150 rtxt.handleClick(w, rect.Rect{X: p.X, Y: p.Y, H: line.h + rowSpacing, W: line.w[i]}, in, siter.styleSel, line, i, &hovering, &linkClick) 151 152 if hovering { 153 if siter.styleSel.isLink { 154 siter.styleSel.Color = siter.styleSel.hoverColor 155 } 156 if siter.styleSel.Tooltip != nil { 157 w.TooltipOpen(siter.styleSel.TooltipWidth, false, siter.styleSel.Tooltip) 158 } 159 } 160 161 chunkrng := Sel{line.off[i], line.off[i] + chunk.len()} 162 163 simpleDrawChunk := false 164 165 switch insel { 166 case selBefore: 167 if chunkrng.contains(rtxt.Sel.S) { 168 s := rtxt.Sel.S - line.off[i] 169 chunk1 := chunk.sub(0, s) 170 w1 := line.chunkWidthEx(i, 0, chunk1, rtxt.adv) 171 drawChunk(w, out, &p, chunk1, siter.styleSel, w1, line.h, line.asc) 172 173 if chunkrng.contains(rtxt.Sel.E) { 174 e := rtxt.Sel.E - line.off[i] 175 chunk2 := chunk.sub(s, e) 176 w2 := line.chunkWidthEx(i, s, chunk2, rtxt.adv) 177 rtxt.drawSelectedChunk(w, out, &p, chunk2, siter.styleSel, w2, line.h, line.asc) 178 179 chunk3 := chunk.sub(e, chunk.len()) 180 w3 := line.chunkWidthEx(i, e, chunk3, rtxt.adv) 181 drawChunk(w, out, &p, chunk3, siter.styleSel, w3, line.h, line.asc) 182 insel = selAfter 183 } else { 184 chunk2 := chunk.sub(s, chunk.len()) 185 w2 := line.chunkWidthEx(i, s, chunk2, rtxt.adv) 186 rtxt.drawSelectedChunk(w, out, &p, chunk2, siter.styleSel, w2, line.h, line.asc) 187 insel = selInside 188 } 189 } else { 190 simpleDrawChunk = true 191 } 192 193 case selInside: 194 if chunkrng.contains(rtxt.Sel.E) { 195 e := rtxt.Sel.E - line.off[i] 196 chunk1 := chunk.sub(0, e) 197 w1 := line.chunkWidthEx(i, 0, chunk1, rtxt.adv) 198 rtxt.drawSelectedChunk(w, out, &p, chunk1, siter.styleSel, w1, line.h, line.asc) 199 200 chunk2 := chunk.sub(e, chunk.len()) 201 w2 := line.chunkWidthEx(i, e, chunk2, rtxt.adv) 202 drawChunk(w, out, &p, chunk2, siter.styleSel, w2, line.h, line.asc) 203 insel = selAfter 204 } else if chunkrng.S >= rtxt.Sel.E { 205 insel = selAfter 206 simpleDrawChunk = true 207 } else { 208 rtxt.drawSelectedChunk(w, out, &p, chunk, siter.styleSel, line.w[i], line.h, line.asc) 209 } 210 211 case selAfter: 212 simpleDrawChunk = true 213 214 case selTick: 215 simpleDrawChunk = true 216 } 217 218 if simpleDrawChunk { 219 drawChunk(w, out, &p, chunk, siter.styleSel, line.w[i], line.h, line.asc) 220 if insel == selTick && (rtxt.flags&ShowTick != 0) && (wasFocused || (rtxt.flags&Keyboard == 0)) && chunkrng.contains(rtxt.Sel.S) { 221 x := p.X - line.w[i] + line.chunkWidth(i, rtxt.Sel.S-line.off[i], rtxt.adv) 222 rtxt.drawTick(w, out, image.Point{x, p.Y}, line.h, siter.styleSel.Color, lineidx) 223 } 224 } 225 } 226 227 if len(line.chunks) == 0 && line.sel().contains(rtxt.Sel.S) && insel != selTick { 228 insel = selInside 229 } 230 231 // click after the last chunk of text on the line 232 rtxt.handleClick(w, rect.Rect{X: p.X, Y: p.Y, W: rtxt.width - p.X, H: line.h + rowSpacing}, in, siter.styleSel, line, len(line.chunks)-1, nil, nil) 233 234 if insel == selTick && (rtxt.flags&ShowTick != 0) && (wasFocused || (rtxt.flags&Keyboard == 0)) && (line.endoff() == rtxt.Sel.S) { 235 rtxt.drawTick(w, out, p, line.h, siter.styleSel.Color, lineidx) 236 } 237 238 if insel == selInside && rtxt.Sel.contains(line.endoff()) { 239 spacewidth := int(4 * w.Master().Style().Scaling) 240 out.FillRect(rect.Rect{X: p.X, Y: p.Y, W: spacewidth, H: line.h}, 0, rtxt.selColor) 241 } 242 243 if insel == selInside && line.sel().contains(rtxt.Sel.E) { 244 insel = selAfter 245 } 246 } 247 248 if rtxt.down && !in.Mouse.Down(pointer.ButtonLeft) { 249 rtxt.down = false 250 } 251 252 if rtxt.followCursor { 253 rtxt.followCursor = false 254 if above, below := w.Invisible(); above || below { 255 scrollbary = w.At().Y - w.Bounds.H/2 256 if scrollbary < 0 { 257 scrollbary = 0 258 } 259 } 260 } 261 262 if pageKey != 0 && viewporth > 0 { 263 scrollbary += (pageKey * viewporth) - framework.FontHeight(rtxt.face) 264 if scrollbary < 0 { 265 scrollbary = 0 266 } 267 } else if arrowKey != 0 { 268 scrollbary += arrowKey * framework.FontHeight(rtxt.face) 269 if scrollbary < 0 { 270 scrollbary = 0 271 } 272 } 273 274 if scrollbary != w.Scrollbar.Y { 275 w.Scrollbar.Y = scrollbary 276 w.Master().Changed() 277 } else if wasFocused != rtxt.focused { 278 w.Master().Changed() 279 } 280 281 if linkClick >= 0 { 282 rtxt.ctor = Ctor{rtxt: rtxt, mode: ctorLink, w: w, linkClick: linkClick} 283 return &rtxt.ctor 284 } 285 return nil 286 } 287 288 func (rtxt *RichText) drawTick(w *framework.Window, out *command.Buffer, p image.Point, lineh int, color color.RGBA, lineidx int) { 289 linethick := int(w.Master().Style().Scaling) 290 out.StrokeLine(image.Point{p.X, p.Y}, image.Point{p.X, p.Y + lineh}, linethick, color) 291 } 292 293 func (rtxt *RichText) drawSelectedChunk(w *framework.Window, out *command.Buffer, p *image.Point, chunk chunk, styleSel styleSel, width, lineh, lineasc int) { 294 styleSel.BgColor = rtxt.selColor 295 styleSel.Color = styleSel.SelFgColor 296 drawChunk(w, out, p, chunk, styleSel, width, lineh, lineasc) 297 } 298 299 func drawChunk(w *framework.Window, out *command.Buffer, p *image.Point, chunk chunk, styleSel styleSel, width, lineh, lineasc int) { 300 if chunk.isspacing() { 301 if debugDrawBoundingBoxes && width > 0 { 302 yoff := alignBaseline(lineh, lineasc, styleSel.Face) 303 r := rect.Rect{X: p.X, Y: p.Y + yoff, H: lineh - yoff, W: width} 304 out.FillRect(r, 0, color.RGBA{0xff, 0xff, 0x00, 0xff}) 305 } 306 if styleSel.BgColor != (color.RGBA{}) && width > 0 { 307 r := rect.Rect{X: p.X, Y: p.Y, H: lineh, W: width} 308 out.FillRect(r, 0, styleSel.BgColor) 309 } 310 } else { 311 r := rect.Rect{X: p.X, Y: p.Y, H: lineh, W: width} 312 313 if styleSel.BgColor != (color.RGBA{}) { 314 out.FillRect(r, 0, styleSel.BgColor) 315 } 316 317 yoff := alignBaseline(lineh, lineasc, styleSel.Face) 318 r.Y += yoff 319 r.H -= yoff 320 321 if debugDrawBoundingBoxes { 322 out.FillRect(r, 0, color.RGBA{0xff, 0, 0, 0xff}) 323 } 324 325 if chunk.b != nil { 326 //TODO: DrawTextBytes 327 panic("not implemented") 328 } else { 329 out.DrawText(r, chunk.s, styleSel.Face, styleSel.Color) 330 } 331 332 if styleSel.Flags&Underline != 0 { 333 linethick := int(w.Master().Style().Scaling) 334 y := p.Y + lineh 335 out.StrokeLine(image.Point{p.X, y}, image.Point{p.X + width, y}, linethick, styleSel.Color) 336 } 337 338 if styleSel.Flags&Strikethrough != 0 { 339 linethick := int(w.Master().Style().Scaling) 340 m := styleSel.Face.Metrics() 341 y := p.Y + lineasc + m.Descent.Ceil() - m.Ascent.Ceil()/2 342 out.StrokeLine(image.Point{p.X, y}, image.Point{p.X + width, y}, linethick, styleSel.Color) 343 } 344 } 345 346 p.X += width 347 } 348 349 func alignBaseline(h int, asc int, face font.Face) int { 350 d := asc - face.Metrics().Ascent.Ceil() 351 if d < 0 { 352 d = 0 353 } 354 return d 355 } 356 357 func (rtxt *RichText) reflow() { 358 if rtxt.lines != nil { 359 rtxt.lines = rtxt.lines[:0] 360 } 361 362 rtxt.maxWidth = rtxt.width 363 364 var ln line 365 366 var siter styleIterator 367 siter.Init(rtxt) 368 369 runeoff := 0 370 var splitruneoff int 371 if (rtxt.flags&AutoWrap != 0) && (siter.styleSel.align != AlignLeftDumb) { 372 splitruneoff = rtxt.wordwrap(rtxt.chunks, 0, runeoff) 373 } 374 375 h := []int{} 376 asc := []int{} 377 378 var linew fixed.Int26_6 379 380 maxint := func(v []int) int { 381 m := 0 382 for i := range v { 383 if v[i] > m { 384 m = v[i] 385 } 386 } 387 return m 388 } 389 390 lastEmptyChunkOff := int32(0) 391 392 flushLine := func(runedelta int) { 393 lnwidth := ln.width() 394 diff := rtxt.width - lnwidth 395 switch siter.styleSel.align { 396 case AlignRight: 397 if diff > 0 { 398 ln.leftMargin = diff 399 } 400 case AlignCenter: 401 if diff > 2 { 402 ln.leftMargin = diff / 2 403 } 404 case AlignJustified: 405 if runeoff+runedelta == splitruneoff && rtxt.flags&AutoWrap != 0 { 406 justifyLine(ln, diff) 407 } 408 } 409 if len(ln.chunks) == 0 { 410 ln.h = framework.FontHeight(siter.styleSel.Face) 411 ln.off = append(ln.off, lastEmptyChunkOff) 412 } else { 413 ln.h = maxint(h) 414 ln.asc = maxint(asc) 415 } 416 if rtxt.maxWidth < lnwidth { 417 rtxt.maxWidth = lnwidth 418 } 419 rtxt.lines = append(rtxt.lines, ln) 420 ln = line{} 421 h = h[:0] 422 asc = asc[:0] 423 ln.runeoff = runeoff + runedelta 424 linew = fixed.I(0) 425 } 426 427 off := int32(0) 428 429 for i, chunk := range rtxt.chunks { 430 // Note chunk is a copy of the element in the slice so we can modify it with impunity 431 432 start := int32(0) 433 j := int32(0) 434 var chunkw fixed.Int26_6 435 436 flushChunk := func(end int32, styleSel styleSel) { 437 if start != end { 438 ln.chunks = append(ln.chunks, rtxt.chunks[i].sub(start, end)) 439 ln.off = append(ln.off, off+start) 440 ln.w = append(ln.w, chunkw.Ceil()) 441 h = append(h, framework.FontHeight(styleSel.Face)) 442 asc = append(asc, styleSel.Face.Metrics().Ascent.Ceil()) 443 linew += chunkw 444 chunkw = fixed.I(0) 445 start = end 446 } else { 447 lastEmptyChunkOff = off + start 448 } 449 } 450 451 for j < chunk.len() { 452 var ch rune 453 var rsz int 454 if chunk.b != nil { 455 ch, rsz = utf8.DecodeRune(chunk.b[j:]) 456 j += int32(rsz) 457 } else { 458 ch, rsz = utf8.DecodeRuneInString(chunk.s[j:]) 459 j += int32(rsz) 460 } 461 a := rtxt.adv[runeoff] 462 runeoff++ 463 464 doWordWrap := false 465 466 switch ch { 467 case '\t': 468 flushChunk(j-1, siter.styleSel) 469 chunkw = a 470 flushChunk(j, siter.styleSel) 471 case '\n': 472 flushChunk(j-1, siter.styleSel) 473 start++ // skip newline 474 flushLine(0) 475 doWordWrap = true 476 default: 477 chunkw += a 478 } 479 480 if rtxt.Flags&AutoWrap != 0 { 481 if siter.styleSel.align == AlignLeftDumb && (linew+chunkw).Ceil() > rtxt.width && (j-int32(rsz)-start) > 0 { 482 chunkw -= a 483 flushChunk(j-int32(rsz), siter.styleSel) 484 flushLine(-1) 485 doWordWrap = true 486 chunkw += a 487 } else if runeoff == splitruneoff { 488 if ch == ' ' && siter.styleSel.align == AlignJustified { 489 chunkw -= a 490 flushChunk(j-1, siter.styleSel) 491 chunkw += a 492 } 493 flushChunk(j, siter.styleSel) 494 flushLine(0) 495 doWordWrap = true 496 } else if ch == ' ' && siter.styleSel.align == AlignJustified { 497 chunkw -= a 498 flushChunk(j-1, siter.styleSel) 499 chunkw += a 500 flushChunk(j, siter.styleSel) 501 } 502 } 503 504 styleSel := siter.styleSel 505 if siter.AdvanceRune(rsz) { 506 flushChunk(j, styleSel) 507 } 508 509 if doWordWrap && (rtxt.flags&AutoWrap != 0) && siter.styleSel.align != AlignLeftDumb { 510 splitruneoff = rtxt.wordwrap(rtxt.chunks[i:], j, runeoff) 511 } 512 } 513 514 flushChunk(rtxt.chunks[i].len(), siter.styleSel) 515 off += rtxt.chunks[i].len() 516 } 517 518 if len(ln.chunks) > 0 { 519 flushLine(0) 520 } 521 } 522 523 func (rtxt *RichText) wordwrap(chunks []chunk, start int32, startRuneoff int) int { 524 runeoff := startRuneoff 525 spaceruneoff := -1 526 advance := fixed.I(0) 527 for _, chunk := range chunks { 528 if chunk.b != nil { 529 chunk.b = chunk.b[start:] 530 } else { 531 chunk.s = chunk.s[start:] 532 } 533 start = 0 534 535 for chunk.len() > 0 { 536 var ch rune 537 var rsz int 538 if chunk.b != nil { 539 ch, rsz = utf8.DecodeRune(chunk.b) 540 chunk.b = chunk.b[rsz:] 541 } else { 542 ch, rsz = utf8.DecodeRuneInString(chunk.s) 543 chunk.s = chunk.s[rsz:] 544 } 545 a := rtxt.adv[runeoff] 546 runeoff++ 547 548 if ch == '\n' { 549 return -1 550 } 551 if ch == ' ' { 552 spaceruneoff = runeoff 553 } 554 555 advance += a 556 557 if advance.Ceil() > rtxt.width { 558 if spaceruneoff > 0 { 559 return spaceruneoff 560 } else { 561 return runeoff - 1 562 } 563 } 564 } 565 } 566 return -1 567 } 568 569 func justifyLine(line line, diff int) { 570 if len(line.chunks) == 0 { 571 return 572 } 573 574 nspaces := 0 575 for i := range line.chunks { 576 if line.chunks[i].b != nil { 577 if len(line.chunks[i].b) == 1 { 578 switch line.chunks[i].b[0] { 579 case '\t': 580 return 581 case ' ': 582 nspaces++ 583 } 584 } 585 } else { 586 switch line.chunks[i].s { 587 case "\t": 588 return 589 case " ": 590 nspaces++ 591 } 592 } 593 } 594 595 isspc := func(chunk chunk) bool { 596 if chunk.b != nil { 597 return (len(chunk.b) == 1) && (chunk.b[0] == ' ') 598 } else { 599 return chunk.s == " " 600 } 601 } 602 603 for i := len(line.chunks) - 1; i > 0; i-- { 604 if isspc(line.chunks[i]) { 605 diff += line.w[i] 606 line.w[i] = 0 607 nspaces-- 608 } else { 609 break 610 } 611 } 612 613 if nspaces == 0 { 614 return 615 } 616 617 deltaw := float64(diff) / float64(nspaces) 618 udiff := 0 619 deltaerr := deltaw - float64(int(deltaw)) 620 error := float64(0) 621 622 for i := range line.chunks { 623 if isspc(line.chunks[i]) && line.w[i] > 0 { 624 line.w[i] += int(deltaw) 625 udiff += int(deltaw) 626 error += deltaerr 627 if error > 1 { 628 line.w[i]++ 629 udiff++ 630 error -= 1 631 } 632 } 633 } 634 635 if diff > udiff { 636 for i := range line.chunks { 637 if isspc(line.chunks[i]) && line.w[i] > 0 { 638 line.w[i] += (diff - udiff) 639 break 640 } 641 } 642 } 643 } 644 645 type styleIterator struct { 646 rtxt *RichText 647 styleSels []styleSel 648 styleSel styleSel 649 cur int32 650 } 651 652 func (siter *styleIterator) Init(rtxt *RichText) { 653 siter.styleSels = rtxt.styleSels 654 siter.rtxt = rtxt 655 siter.setStyle() 656 } 657 658 func (siter *styleIterator) setStyle() { 659 if len(siter.styleSels) == 0 || siter.styleSels[0].S > siter.cur { 660 siter.defaultStyle() 661 } else { 662 siter.styleSel = siter.styleSels[0] 663 siter.fixDefaults() 664 } 665 } 666 667 func (siter *styleIterator) defaultStyle() { 668 if len(siter.styleSels) > 0 { 669 siter.styleSel.E = siter.styleSels[0].S 670 } else { 671 siter.styleSel.E = int32(^uint32(0) >> 1) 672 } 673 siter.styleSel.align = AlignLeftDumb 674 siter.styleSel.Face = siter.rtxt.face 675 siter.styleSel.Flags = 0 676 siter.styleSel.link = nil 677 siter.styleSel.Color = siter.rtxt.txtColor 678 siter.styleSel.SelFgColor = siter.rtxt.selFgColor 679 siter.styleSel.BgColor = color.RGBA{0, 0, 0, 0} 680 } 681 682 func (siter *styleIterator) fixDefaults() { 683 zero := color.RGBA{} 684 if siter.styleSel.Color == zero { 685 siter.styleSel.Color = siter.rtxt.txtColor 686 } 687 if siter.styleSel.SelFgColor == zero { 688 siter.styleSel.SelFgColor = siter.rtxt.selFgColor 689 } 690 if siter.styleSel.BgColor == zero { 691 siter.styleSel.BgColor = color.RGBA{0, 0, 0, 0} 692 } 693 if siter.styleSel.Face == (font.Face{}) { 694 siter.styleSel.Face = siter.rtxt.face 695 } 696 } 697 698 func (siter *styleIterator) AdvanceRune(sz int) bool { 699 siter.cur += int32(sz) 700 return siter.AdvanceTo(siter.cur) 701 } 702 703 func (siter *styleIterator) AdvanceTo(pos int32) bool { 704 siter.cur = pos 705 if siter.cur < siter.styleSel.E { 706 return false 707 } 708 for len(siter.styleSels) > 0 && siter.cur >= siter.styleSels[0].E { 709 siter.styleSels = siter.styleSels[1:] 710 } 711 siter.setStyle() 712 return true 713 } 714 715 func (line line) chunkWidth(chunkIdx int, byteIdx int32, adv []fixed.Int26_6) int { 716 _, runeoff := line.chunkAdvance(chunkIdx) 717 if len(line.chunks) == 0 { 718 return 0 719 } 720 chunk := line.chunks[chunkIdx] 721 722 w := fixed.I(0) 723 off := int32(0) 724 for chunk.len() > 0 { 725 if off >= byteIdx { 726 return w.Ceil() 727 } 728 729 w += adv[runeoff] 730 731 var rsz int 732 if chunk.b != nil { 733 _, rsz = utf8.DecodeRune(chunk.b) 734 chunk.b = chunk.b[rsz:] 735 } else { 736 _, rsz = utf8.DecodeRuneInString(chunk.s) 737 chunk.s = chunk.s[rsz:] 738 } 739 off += int32(rsz) 740 runeoff++ 741 } 742 743 return w.Ceil() 744 } 745 746 func (line line) chunkWidthEx(chunkIdx int, startByteIdx int32, tgtChunk chunk, adv []fixed.Int26_6) int { 747 _, runeoff := line.chunkAdvance(chunkIdx) 748 if len(line.chunks) == 0 { 749 return 0 750 } 751 off := int32(0) 752 753 { 754 chunk := line.chunks[chunkIdx] 755 756 for chunk.len() > 0 { 757 if off >= startByteIdx { 758 break 759 } 760 761 var rsz int 762 if chunk.b != nil { 763 _, rsz = utf8.DecodeRune(chunk.b) 764 chunk.b = chunk.b[rsz:] 765 } else { 766 _, rsz = utf8.DecodeRuneInString(chunk.s) 767 chunk.s = chunk.s[rsz:] 768 } 769 off += int32(rsz) 770 runeoff++ 771 } 772 } 773 w := fixed.I(0) 774 775 for tgtChunk.len() > 0 { 776 w += adv[runeoff] 777 778 var rsz int 779 if tgtChunk.b != nil { 780 _, rsz = utf8.DecodeRune(tgtChunk.b) 781 tgtChunk.b = tgtChunk.b[rsz:] 782 } else { 783 _, rsz = utf8.DecodeRuneInString(tgtChunk.s) 784 tgtChunk.s = tgtChunk.s[rsz:] 785 } 786 787 runeoff++ 788 } 789 790 return w.Ceil() 791 } 792 793 func (line line) chunkAdvance(chunkIdx int) (int, int) { 794 runeoff := line.runeoff 795 w := 0 796 for i := 0; i < chunkIdx; i++ { 797 w += line.w[i] 798 runeoff += line.chunks[i].runeCount() 799 } 800 return w, runeoff 801 }