gioui.org/ui@v0.0.0-20190926171558-ce74bc0cbaea/text/editor.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  package text
     4  
     5  import (
     6  	"image"
     7  	"image/color"
     8  	"math"
     9  	"time"
    10  	"unicode/utf8"
    11  
    12  	"gioui.org/ui"
    13  	"gioui.org/ui/gesture"
    14  	"gioui.org/ui/key"
    15  	"gioui.org/ui/layout"
    16  	"gioui.org/ui/paint"
    17  	"gioui.org/ui/pointer"
    18  
    19  	"golang.org/x/image/math/fixed"
    20  )
    21  
    22  // Editor implements an editable and scrollable text area.
    23  type Editor struct {
    24  	Face      Face
    25  	Alignment Alignment
    26  	// SingleLine force the text to stay on a single line.
    27  	// SingleLine also sets the scrolling direction to
    28  	// horizontal.
    29  	SingleLine bool
    30  	// Submit enabled translation of carriage return keys to SubmitEvents.
    31  	// If not enabled, carriage returns are inserted as newlines in the text.
    32  	Submit bool
    33  
    34  	// Material for drawing the text.
    35  	Material ui.MacroOp
    36  	// Hint contains the text displayed to the user when the
    37  	// Editor is empty.
    38  	Hint string
    39  	// Mmaterial is used to draw the hint.
    40  	HintMaterial ui.MacroOp
    41  
    42  	oldScale          int
    43  	blinkStart        time.Time
    44  	focused           bool
    45  	rr                editBuffer
    46  	maxWidth          int
    47  	viewSize          image.Point
    48  	valid             bool
    49  	lines             []Line
    50  	dims              layout.Dimensions
    51  	padTop, padBottom int
    52  	padLeft, padRight int
    53  	requestFocus      bool
    54  
    55  	it lineIterator
    56  
    57  	// carXOff is the offset to the current caret
    58  	// position when moving between lines.
    59  	carXOff fixed.Int26_6
    60  
    61  	scroller  gesture.Scroll
    62  	scrollOff image.Point
    63  
    64  	clicker gesture.Click
    65  
    66  	// events is the list of events not yet processed.
    67  	events []ui.Event
    68  }
    69  
    70  type EditorEvent interface {
    71  	isEditorEvent()
    72  }
    73  
    74  // A ChangeEvent is generated for every user change to the text.
    75  type ChangeEvent struct{}
    76  
    77  // A SubmitEvent is generated when Submit is set
    78  // and a carriage return key is pressed.
    79  type SubmitEvent struct{}
    80  
    81  const (
    82  	blinksPerSecond  = 1
    83  	maxBlinkDuration = 10 * time.Second
    84  )
    85  
    86  // Event returns the next available editor event, or false if none are available.
    87  func (e *Editor) Event(gtx *layout.Context) (EditorEvent, bool) {
    88  	// Crude configuration change detection.
    89  	if scale := gtx.Px(ui.Sp(100)); scale != e.oldScale {
    90  		e.invalidate()
    91  		e.oldScale = scale
    92  	}
    93  	sbounds := e.scrollBounds()
    94  	var smin, smax int
    95  	var axis gesture.Axis
    96  	if e.SingleLine {
    97  		axis = gesture.Horizontal
    98  		smin, smax = sbounds.Min.X, sbounds.Max.X
    99  	} else {
   100  		axis = gesture.Vertical
   101  		smin, smax = sbounds.Min.Y, sbounds.Max.Y
   102  	}
   103  	sdist := e.scroller.Scroll(gtx.Config, gtx.Queue, axis)
   104  	var soff int
   105  	if e.SingleLine {
   106  		e.scrollOff.X += sdist
   107  		soff = e.scrollOff.X
   108  	} else {
   109  		e.scrollOff.Y += sdist
   110  		soff = e.scrollOff.Y
   111  	}
   112  	for _, evt := range e.clicker.Events(gtx.Queue) {
   113  		switch {
   114  		case evt.Type == gesture.TypePress && evt.Source == pointer.Mouse,
   115  			evt.Type == gesture.TypeClick && evt.Source == pointer.Touch:
   116  			e.blinkStart = gtx.Now()
   117  			e.moveCoord(image.Point{
   118  				X: int(math.Round(float64(evt.Position.X))),
   119  				Y: int(math.Round(float64(evt.Position.Y))),
   120  			})
   121  			e.requestFocus = true
   122  			if e.scroller.State() != gesture.StateFlinging {
   123  				e.scrollToCaret(gtx)
   124  			}
   125  		}
   126  	}
   127  	if (sdist > 0 && soff >= smax) || (sdist < 0 && soff <= smin) {
   128  		e.scroller.Stop()
   129  	}
   130  	e.events = append(e.events, gtx.Queue.Events(e)...)
   131  	return e.editorEvent(gtx)
   132  }
   133  
   134  func (e *Editor) editorEvent(gtx *layout.Context) (EditorEvent, bool) {
   135  	for len(e.events) > 0 {
   136  		ke := e.events[0]
   137  		copy(e.events, e.events[1:])
   138  		e.events = e.events[:len(e.events)-1]
   139  		e.blinkStart = gtx.Now()
   140  		switch ke := ke.(type) {
   141  		case key.FocusEvent:
   142  			e.focused = ke.Focus
   143  		case key.Event:
   144  			if !e.focused {
   145  				break
   146  			}
   147  			if e.Submit && ke.Name == key.NameReturn || ke.Name == key.NameEnter {
   148  				if !ke.Modifiers.Contain(key.ModShift) {
   149  					return SubmitEvent{}, true
   150  				}
   151  			}
   152  			if e.command(ke) {
   153  				e.scrollToCaret(gtx.Config)
   154  				e.scroller.Stop()
   155  			}
   156  		case key.EditEvent:
   157  			e.scrollToCaret(gtx)
   158  			e.scroller.Stop()
   159  			e.append(ke.Text)
   160  		}
   161  		if e.rr.Changed() {
   162  			return ChangeEvent{}, true
   163  		}
   164  	}
   165  	return nil, false
   166  }
   167  
   168  func (e *Editor) caretWidth(c ui.Config) fixed.Int26_6 {
   169  	oneDp := c.Px(ui.Dp(1))
   170  	return fixed.Int26_6(oneDp * 64)
   171  }
   172  
   173  // Focus requests the input focus for the Editor.
   174  func (e *Editor) Focus() {
   175  	e.requestFocus = true
   176  }
   177  
   178  // Layout flushes any remaining events and lays out the editor.
   179  func (e *Editor) Layout(gtx *layout.Context) {
   180  	cs := gtx.Constraints
   181  	for _, ok := e.Event(gtx); ok; _, ok = e.Event(gtx) {
   182  	}
   183  	twoDp := gtx.Px(ui.Dp(2))
   184  	e.padLeft, e.padRight = twoDp, twoDp
   185  	maxWidth := cs.Width.Max
   186  	if e.SingleLine {
   187  		maxWidth = inf
   188  	}
   189  	if maxWidth != inf {
   190  		maxWidth -= e.padLeft + e.padRight
   191  	}
   192  	if maxWidth != e.maxWidth {
   193  		e.maxWidth = maxWidth
   194  		e.invalidate()
   195  	}
   196  
   197  	e.layout()
   198  	lines, size := e.lines, e.dims.Size
   199  	e.viewSize = cs.Constrain(size)
   200  
   201  	carLine, _, carX, carY := e.layoutCaret()
   202  
   203  	off := image.Point{
   204  		X: -e.scrollOff.X + e.padLeft,
   205  		Y: -e.scrollOff.Y + e.padTop,
   206  	}
   207  	clip := image.Rectangle{
   208  		Min: image.Point{X: 0, Y: 0},
   209  		Max: image.Point{X: e.viewSize.X, Y: e.viewSize.Y},
   210  	}
   211  	key.InputOp{Key: e, Focus: e.requestFocus}.Add(gtx.Ops)
   212  	e.requestFocus = false
   213  	e.it = lineIterator{
   214  		Lines:     lines,
   215  		Clip:      clip,
   216  		Alignment: e.Alignment,
   217  		Width:     e.viewWidth(),
   218  		Offset:    off,
   219  	}
   220  	var stack ui.StackOp
   221  	stack.Push(gtx.Ops)
   222  	// Apply material. Set a default color in case the material is empty.
   223  	if e.rr.len() > 0 {
   224  		paint.ColorOp{Color: color.RGBA{A: 0xff}}.Add(gtx.Ops)
   225  		e.Material.Add(gtx.Ops)
   226  	} else {
   227  		paint.ColorOp{Color: color.RGBA{A: 0xaa}}.Add(gtx.Ops)
   228  		e.HintMaterial.Add(gtx.Ops)
   229  	}
   230  	for {
   231  		str, lineOff, ok := e.it.Next()
   232  		if !ok {
   233  			break
   234  		}
   235  		var stack ui.StackOp
   236  		stack.Push(gtx.Ops)
   237  		ui.TransformOp{}.Offset(lineOff).Add(gtx.Ops)
   238  		e.Face.Path(str).Add(gtx.Ops)
   239  		paint.PaintOp{Rect: toRectF(clip).Sub(lineOff)}.Add(gtx.Ops)
   240  		stack.Pop()
   241  	}
   242  	if e.focused {
   243  		now := gtx.Now()
   244  		dt := now.Sub(e.blinkStart)
   245  		blinking := dt < maxBlinkDuration
   246  		const timePerBlink = time.Second / blinksPerSecond
   247  		nextBlink := now.Add(timePerBlink/2 - dt%(timePerBlink/2))
   248  		on := !blinking || dt%timePerBlink < timePerBlink/2
   249  		if on {
   250  			carWidth := e.caretWidth(gtx)
   251  			carX -= carWidth / 2
   252  			carAsc, carDesc := -lines[carLine].Bounds.Min.Y, lines[carLine].Bounds.Max.Y
   253  			carRect := image.Rectangle{
   254  				Min: image.Point{X: carX.Ceil(), Y: carY - carAsc.Ceil()},
   255  				Max: image.Point{X: carX.Ceil() + carWidth.Ceil(), Y: carY + carDesc.Ceil()},
   256  			}
   257  			carRect = carRect.Add(image.Point{
   258  				X: -e.scrollOff.X + e.padLeft,
   259  				Y: -e.scrollOff.Y + e.padTop,
   260  			})
   261  			carRect = clip.Intersect(carRect)
   262  			if !carRect.Empty() {
   263  				paint.ColorOp{Color: color.RGBA{A: 0xff}}.Add(gtx.Ops)
   264  				e.Material.Add(gtx.Ops)
   265  				paint.PaintOp{Rect: toRectF(carRect)}.Add(gtx.Ops)
   266  			}
   267  		}
   268  		if blinking {
   269  			redraw := ui.InvalidateOp{At: nextBlink}
   270  			redraw.Add(gtx.Ops)
   271  		}
   272  	}
   273  	stack.Pop()
   274  
   275  	baseline := e.padTop + e.dims.Baseline
   276  	pointerPadding := gtx.Px(ui.Dp(4))
   277  	r := image.Rectangle{Max: e.viewSize}
   278  	r.Min.X -= pointerPadding
   279  	r.Min.Y -= pointerPadding
   280  	r.Max.X += pointerPadding
   281  	r.Max.X += pointerPadding
   282  	pointer.RectAreaOp{Rect: r}.Add(gtx.Ops)
   283  	e.scroller.Add(gtx.Ops)
   284  	e.clicker.Add(gtx.Ops)
   285  	gtx.Dimensions = layout.Dimensions{Size: e.viewSize, Baseline: baseline}
   286  }
   287  
   288  // Text returns the contents of the editor.
   289  func (e *Editor) Text() string {
   290  	return e.rr.String()
   291  }
   292  
   293  // SetText replaces the contents of the editor.
   294  func (e *Editor) SetText(s string) {
   295  	e.rr = editBuffer{}
   296  	e.carXOff = 0
   297  	e.prepend(s)
   298  }
   299  
   300  func (e *Editor) layout() {
   301  	e.adjustScroll()
   302  	if e.valid {
   303  		return
   304  	}
   305  	e.layoutText()
   306  	e.valid = true
   307  }
   308  
   309  func (e *Editor) scrollBounds() image.Rectangle {
   310  	var b image.Rectangle
   311  	if e.SingleLine {
   312  		if len(e.lines) > 0 {
   313  			b.Min.X = align(e.Alignment, e.lines[0].Width, e.viewWidth()).Floor()
   314  			if b.Min.X > 0 {
   315  				b.Min.X = 0
   316  			}
   317  		}
   318  		b.Max.X = e.dims.Size.X + b.Min.X - e.viewSize.X
   319  	} else {
   320  		b.Max.Y = e.dims.Size.Y - e.viewSize.Y
   321  	}
   322  	return b
   323  }
   324  
   325  func (e *Editor) adjustScroll() {
   326  	b := e.scrollBounds()
   327  	if e.scrollOff.X > b.Max.X {
   328  		e.scrollOff.X = b.Max.X
   329  	}
   330  	if e.scrollOff.X < b.Min.X {
   331  		e.scrollOff.X = b.Min.X
   332  	}
   333  	if e.scrollOff.Y > b.Max.Y {
   334  		e.scrollOff.Y = b.Max.Y
   335  	}
   336  	if e.scrollOff.Y < b.Min.Y {
   337  		e.scrollOff.Y = b.Min.Y
   338  	}
   339  }
   340  
   341  func (e *Editor) moveCoord(pos image.Point) {
   342  	e.layout()
   343  	var (
   344  		prevDesc fixed.Int26_6
   345  		carLine  int
   346  		y        int
   347  	)
   348  	for _, l := range e.lines {
   349  		y += (prevDesc + l.Ascent).Ceil()
   350  		prevDesc = l.Descent
   351  		if y+prevDesc.Ceil() >= pos.Y+e.scrollOff.Y-e.padTop {
   352  			break
   353  		}
   354  		carLine++
   355  	}
   356  	x := fixed.I(pos.X + e.scrollOff.X - e.padLeft)
   357  	e.moveToLine(x, carLine)
   358  }
   359  
   360  func (e *Editor) layoutText() {
   361  	s := e.rr.String()
   362  	if s == "" {
   363  		s = e.Hint
   364  	}
   365  	textLayout := e.Face.Layout(s, LayoutOptions{SingleLine: e.SingleLine, MaxWidth: e.maxWidth})
   366  	lines := textLayout.Lines
   367  	dims := linesDimens(lines)
   368  	for i := 0; i < len(lines)-1; i++ {
   369  		s := lines[i].Text.String
   370  		// To avoid layout flickering while editing, assume a soft newline takes
   371  		// up all available space.
   372  		if len(s) > 0 {
   373  			r, _ := utf8.DecodeLastRuneInString(s)
   374  			if r != '\n' {
   375  				dims.Size.X = e.maxWidth
   376  				break
   377  			}
   378  		}
   379  	}
   380  	padTop, padBottom := textPadding(lines)
   381  	dims.Size.Y += padTop + padBottom
   382  	dims.Size.X += e.padLeft + e.padRight
   383  	e.padTop = padTop
   384  	e.padBottom = padBottom
   385  	e.lines, e.dims = lines, dims
   386  }
   387  
   388  func (e *Editor) viewWidth() int {
   389  	return e.viewSize.X - e.padLeft - e.padRight
   390  }
   391  
   392  func (e *Editor) layoutCaret() (carLine, carCol int, x fixed.Int26_6, y int) {
   393  	e.layout()
   394  	var idx int
   395  	var prevDesc fixed.Int26_6
   396  loop:
   397  	for carLine = 0; carLine < len(e.lines); carLine++ {
   398  		l := e.lines[carLine]
   399  		y += (prevDesc + l.Ascent).Ceil()
   400  		prevDesc = l.Descent
   401  		if carLine == len(e.lines)-1 || idx+len(l.Text.String) > e.rr.caret {
   402  			str := l.Text.String
   403  			for _, adv := range l.Text.Advances {
   404  				if idx == e.rr.caret {
   405  					break loop
   406  				}
   407  				x += adv
   408  				_, s := utf8.DecodeRuneInString(str)
   409  				idx += s
   410  				str = str[s:]
   411  				carCol++
   412  			}
   413  			break
   414  		}
   415  		idx += len(l.Text.String)
   416  	}
   417  	x += align(e.Alignment, e.lines[carLine].Width, e.viewWidth())
   418  	return
   419  }
   420  
   421  func (e *Editor) invalidate() {
   422  	e.valid = false
   423  }
   424  
   425  func (e *Editor) deleteRune() {
   426  	e.rr.deleteRune()
   427  	e.carXOff = 0
   428  	e.invalidate()
   429  }
   430  
   431  func (e *Editor) deleteRuneForward() {
   432  	e.rr.deleteRuneForward()
   433  	e.carXOff = 0
   434  	e.invalidate()
   435  }
   436  
   437  func (e *Editor) append(s string) {
   438  	if e.SingleLine && s == "\n" {
   439  		return
   440  	}
   441  	e.prepend(s)
   442  	e.rr.caret += len(s)
   443  }
   444  
   445  func (e *Editor) prepend(s string) {
   446  	e.rr.prepend(s)
   447  	e.carXOff = 0
   448  	e.invalidate()
   449  }
   450  
   451  func (e *Editor) movePages(pages int) {
   452  	e.layout()
   453  	_, _, carX, carY := e.layoutCaret()
   454  	y := carY + pages*e.viewSize.Y
   455  	var (
   456  		prevDesc fixed.Int26_6
   457  		carLine2 int
   458  	)
   459  	y2 := e.lines[0].Ascent.Ceil()
   460  	for i := 1; i < len(e.lines); i++ {
   461  		if y2 >= y {
   462  			break
   463  		}
   464  		l := e.lines[i]
   465  		h := (prevDesc + l.Ascent).Ceil()
   466  		prevDesc = l.Descent
   467  		if y2+h-y >= y-y2 {
   468  			break
   469  		}
   470  		y2 += h
   471  		carLine2++
   472  	}
   473  	e.carXOff = e.moveToLine(carX+e.carXOff, carLine2)
   474  }
   475  
   476  func (e *Editor) moveToLine(carX fixed.Int26_6, carLine2 int) fixed.Int26_6 {
   477  	e.layout()
   478  	carLine, carCol, _, _ := e.layoutCaret()
   479  	if carLine2 < 0 {
   480  		carLine2 = 0
   481  	}
   482  	if carLine2 >= len(e.lines) {
   483  		carLine2 = len(e.lines) - 1
   484  	}
   485  	// Move to start of line.
   486  	for i := carCol - 1; i >= 0; i-- {
   487  		_, s := e.rr.runeBefore(e.rr.caret)
   488  		e.rr.caret -= s
   489  	}
   490  	if carLine2 != carLine {
   491  		// Move to start of line2.
   492  		if carLine2 > carLine {
   493  			for i := carLine; i < carLine2; i++ {
   494  				e.rr.caret += len(e.lines[i].Text.String)
   495  			}
   496  		} else {
   497  			for i := carLine - 1; i >= carLine2; i-- {
   498  				e.rr.caret -= len(e.lines[i].Text.String)
   499  			}
   500  		}
   501  	}
   502  	l2 := e.lines[carLine2]
   503  	carX2 := align(e.Alignment, l2.Width, e.viewWidth())
   504  	// Only move past the end of the last line
   505  	end := 0
   506  	if carLine2 < len(e.lines)-1 {
   507  		end = 1
   508  	}
   509  	// Move to rune closest to previous horizontal position.
   510  	for i := 0; i < len(l2.Text.Advances)-end; i++ {
   511  		adv := l2.Text.Advances[i]
   512  		if carX2 >= carX {
   513  			break
   514  		}
   515  		if carX2+adv-carX >= carX-carX2 {
   516  			break
   517  		}
   518  		carX2 += adv
   519  		_, s := e.rr.runeAt(e.rr.caret)
   520  		e.rr.caret += s
   521  	}
   522  	return carX - carX2
   523  }
   524  
   525  func (e *Editor) moveLeft() {
   526  	e.rr.moveLeft()
   527  	e.carXOff = 0
   528  }
   529  
   530  func (e *Editor) moveRight() {
   531  	e.rr.moveRight()
   532  	e.carXOff = 0
   533  }
   534  
   535  func (e *Editor) moveStart() {
   536  	carLine, carCol, x, _ := e.layoutCaret()
   537  	advances := e.lines[carLine].Text.Advances
   538  	for i := carCol - 1; i >= 0; i-- {
   539  		_, s := e.rr.runeBefore(e.rr.caret)
   540  		e.rr.caret -= s
   541  		x -= advances[i]
   542  	}
   543  	e.carXOff = -x
   544  }
   545  
   546  func (e *Editor) moveEnd() {
   547  	carLine, carCol, x, _ := e.layoutCaret()
   548  	l := e.lines[carLine]
   549  	// Only move past the end of the last line
   550  	end := 0
   551  	if carLine < len(e.lines)-1 {
   552  		end = 1
   553  	}
   554  	for i := carCol; i < len(l.Text.Advances)-end; i++ {
   555  		adv := l.Text.Advances[i]
   556  		_, s := e.rr.runeAt(e.rr.caret)
   557  		e.rr.caret += s
   558  		x += adv
   559  	}
   560  	a := align(e.Alignment, l.Width, e.viewWidth())
   561  	e.carXOff = l.Width + a - x
   562  }
   563  
   564  func (e *Editor) scrollToCaret(c ui.Config) {
   565  	carWidth := e.caretWidth(c)
   566  	carLine, _, x, y := e.layoutCaret()
   567  	l := e.lines[carLine]
   568  	if e.SingleLine {
   569  		minx := (x - carWidth/2).Ceil()
   570  		if d := minx - e.scrollOff.X + e.padLeft; d < 0 {
   571  			e.scrollOff.X += d
   572  		}
   573  		maxx := (x + carWidth/2).Ceil()
   574  		if d := maxx - (e.scrollOff.X + e.viewSize.X - e.padRight); d > 0 {
   575  			e.scrollOff.X += d
   576  		}
   577  	} else {
   578  		miny := y + l.Bounds.Min.Y.Floor()
   579  		if d := miny - e.scrollOff.Y + e.padTop; d < 0 {
   580  			e.scrollOff.Y += d
   581  		}
   582  		maxy := y + l.Bounds.Max.Y.Ceil()
   583  		if d := maxy - (e.scrollOff.Y + e.viewSize.Y - e.padBottom); d > 0 {
   584  			e.scrollOff.Y += d
   585  		}
   586  	}
   587  }
   588  
   589  func (e *Editor) command(k key.Event) bool {
   590  	switch k.Name {
   591  	case key.NameReturn, key.NameEnter:
   592  		e.append("\n")
   593  	case key.NameDeleteBackward:
   594  		e.deleteRune()
   595  	case key.NameDeleteForward:
   596  		e.deleteRuneForward()
   597  	case key.NameUpArrow:
   598  		line, _, carX, _ := e.layoutCaret()
   599  		e.carXOff = e.moveToLine(carX+e.carXOff, line-1)
   600  	case key.NameDownArrow:
   601  		line, _, carX, _ := e.layoutCaret()
   602  		e.carXOff = e.moveToLine(carX+e.carXOff, line+1)
   603  	case key.NameLeftArrow:
   604  		e.moveLeft()
   605  	case key.NameRightArrow:
   606  		e.moveRight()
   607  	case key.NamePageUp:
   608  		e.movePages(-1)
   609  	case key.NamePageDown:
   610  		e.movePages(+1)
   611  	case key.NameHome:
   612  		e.moveStart()
   613  	case key.NameEnd:
   614  		e.moveEnd()
   615  	default:
   616  		return false
   617  	}
   618  	return true
   619  }
   620  
   621  func (s ChangeEvent) isEditorEvent() {}
   622  func (s SubmitEvent) isEditorEvent() {}