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  }