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  }