editor.go raw
1 // SPDX-License-Identifier: Unlicense OR MIT
2
3 package widget
4
5 import (
6 "bufio"
7 "bytes"
8 "image"
9 "io"
10 "math"
11 "runtime"
12 "sort"
13 "strings"
14 "time"
15 "unicode"
16 "unicode/utf8"
17
18 "github.com/p9c/p9/pkg/gel/gio/f32"
19 "github.com/p9c/p9/pkg/gel/gio/gesture"
20 "github.com/p9c/p9/pkg/gel/gio/io/clipboard"
21 "github.com/p9c/p9/pkg/gel/gio/io/event"
22 "github.com/p9c/p9/pkg/gel/gio/io/key"
23 "github.com/p9c/p9/pkg/gel/gio/io/pointer"
24 "github.com/p9c/p9/pkg/gel/gio/layout"
25 "github.com/p9c/p9/pkg/gel/gio/op"
26 "github.com/p9c/p9/pkg/gel/gio/op/clip"
27 "github.com/p9c/p9/pkg/gel/gio/op/paint"
28 "github.com/p9c/p9/pkg/gel/gio/text"
29 "github.com/p9c/p9/pkg/gel/gio/unit"
30
31 "golang.org/x/image/math/fixed"
32 )
33
34 // Editor implements an editable and scrollable text area.
35 type Editor struct {
36 Alignment text.Alignment
37 // SingleLine force the text to stay on a single line.
38 // SingleLine also sets the scrolling direction to
39 // horizontal.
40 SingleLine bool
41 // Submit enabled translation of carriage return keys to SubmitEvents.
42 // If not enabled, carriage returns are inserted as newlines in the text.
43 Submit bool
44 // Mask replaces the visual display of each rune in the contents with the given rune.
45 // Newline characters are not masked. When non-zero, the unmasked contents
46 // are accessed by Len, Text, and SetText.
47 Mask rune
48
49 eventKey int
50 font text.Font
51 shaper text.Shaper
52 textSize fixed.Int26_6
53 blinkStart time.Time
54 focused bool
55 rr editBuffer
56 maskReader maskReader
57 lastMask rune
58 maxWidth int
59 viewSize image.Point
60 valid bool
61 lines []text.Line
62 shapes []line
63 dims layout.Dimensions
64 requestFocus bool
65
66 caret struct {
67 on bool
68 scroll bool
69 // start is the current caret position, and also the start position of
70 // selected text. end is the end positon of selected text. If start.ofs
71 // == end.ofs, then there's no selection. Note that it's possible (and
72 // common) that the caret (start) is after the end, e.g. after
73 // Shift-DownArrow.
74 start combinedPos
75 end combinedPos
76 }
77
78 dragging bool
79 dragger gesture.Drag
80 scroller gesture.Scroll
81 scrollOff image.Point
82
83 clicker gesture.Click
84
85 // events is the list of events not yet processed.
86 events []EditorEvent
87 // prevEvents is the number of events from the previous frame.
88 prevEvents int
89 }
90
91 type maskReader struct {
92 // rr is the underlying reader.
93 rr io.RuneReader
94 maskBuf [utf8.UTFMax]byte
95 // mask is the utf-8 encoded mask rune.
96 mask []byte
97 // overflow contains excess mask bytes left over after the last Read call.
98 overflow []byte
99 }
100
101 // combinedPos is a point in the editor.
102 type combinedPos struct {
103 // editorBuffer offset. The other three fields are based off of this one.
104 ofs int
105
106 // lineCol.Y = line (offset into Editor.lines), and X = col (offset into
107 // Editor.lines[Y])
108 lineCol screenPos
109
110 // Pixel coordinates
111 x fixed.Int26_6
112 y int
113
114 // xoff is the offset to the current position when moving between lines.
115 xoff fixed.Int26_6
116 }
117
118 type selectionAction int
119
120 const (
121 selectionExtend selectionAction = iota
122 selectionClear
123 )
124
125 func (m *maskReader) Reset(r io.RuneReader, mr rune) {
126 m.rr = r
127 n := utf8.EncodeRune(m.maskBuf[:], mr)
128 m.mask = m.maskBuf[:n]
129 }
130
131 // Read reads from the underlying reader and replaces every
132 // rune with the mask rune.
133 func (m *maskReader) Read(b []byte) (n int, err error) {
134 for len(b) > 0 {
135 var replacement []byte
136 if len(m.overflow) > 0 {
137 replacement = m.overflow
138 } else {
139 var r rune
140 r, _, err = m.rr.ReadRune()
141 if err != nil {
142 break
143 }
144 if r == '\n' {
145 replacement = []byte{'\n'}
146 } else {
147 replacement = m.mask
148 }
149 }
150 nn := copy(b, replacement)
151 m.overflow = replacement[nn:]
152 n += nn
153 b = b[nn:]
154 }
155 return n, err
156 }
157
158 type EditorEvent interface {
159 isEditorEvent()
160 }
161
162 // A ChangeEvent is generated for every user change to the text.
163 type ChangeEvent struct{}
164
165 // A SubmitEvent is generated when Submit is set
166 // and a carriage return key is pressed.
167 type SubmitEvent struct {
168 Text string
169 }
170
171 // A SelectEvent is generated when the user selects some text, or changes the
172 // selection (e.g. with a shift-click), including if they remove the
173 // selection. The selected text is not part of the event, on the theory that
174 // it could be a relatively expensive operation (for a large editor), most
175 // applications won't actually care about it, and those that do can call
176 // Editor.SelectedText() (which can be empty).
177 type SelectEvent struct{}
178
179 type line struct {
180 offset image.Point
181 clip op.CallOp
182 selected bool
183 selectionYOffs int
184 selectionSize image.Point
185 }
186
187 const (
188 blinksPerSecond = 1
189 maxBlinkDuration = 10 * time.Second
190 )
191
192 // Events returns available editor events.
193 func (e *Editor) Events() []EditorEvent {
194 events := e.events
195 e.events = nil
196 e.prevEvents = 0
197 return events
198 }
199
200 func (e *Editor) processEvents(gtx layout.Context) {
201 // Flush events from before the previous Layout.
202 n := copy(e.events, e.events[e.prevEvents:])
203 e.events = e.events[:n]
204 e.prevEvents = n
205
206 if e.shaper == nil {
207 // Can't process events without a shaper.
208 return
209 }
210 oldStart, oldLen := min(e.caret.start.ofs, e.caret.end.ofs), e.SelectionLen()
211 e.processPointer(gtx)
212 e.processKey(gtx)
213 // Queue a SelectEvent if the selection changed, including if it went away.
214 if newStart, newLen := min(e.caret.start.ofs, e.caret.end.ofs), e.SelectionLen(); oldStart != newStart || oldLen != newLen {
215 e.events = append(e.events, SelectEvent{})
216 }
217 }
218
219 func (e *Editor) makeValid(positions ...*combinedPos) {
220 if e.valid {
221 return
222 }
223 e.lines, e.dims = e.layoutText(e.shaper)
224 e.makeValidCaret(positions...)
225 e.valid = true
226 }
227
228 func (e *Editor) processPointer(gtx layout.Context) {
229 sbounds := e.scrollBounds()
230 var smin, smax int
231 var axis gesture.Axis
232 if e.SingleLine {
233 axis = gesture.Horizontal
234 smin, smax = sbounds.Min.X, sbounds.Max.X
235 } else {
236 axis = gesture.Vertical
237 smin, smax = sbounds.Min.Y, sbounds.Max.Y
238 }
239 sdist := e.scroller.Scroll(gtx.Metric, gtx, gtx.Now, axis)
240 var soff int
241 if e.SingleLine {
242 e.scrollRel(sdist, 0)
243 soff = e.scrollOff.X
244 } else {
245 e.scrollRel(0, sdist)
246 soff = e.scrollOff.Y
247 }
248 for _, evt := range e.clickDragEvents(gtx) {
249 switch evt := evt.(type) {
250 case gesture.ClickEvent:
251 switch {
252 case evt.Type == gesture.TypePress && evt.Source == pointer.Mouse,
253 evt.Type == gesture.TypeClick:
254 prevCaretPos := e.caret.start
255 e.blinkStart = gtx.Now
256 e.moveCoord(image.Point{
257 X: int(math.Round(float64(evt.Position.X))),
258 Y: int(math.Round(float64(evt.Position.Y))),
259 })
260 e.requestFocus = true
261 if e.scroller.State() != gesture.StateFlinging {
262 e.caret.scroll = true
263 }
264
265 if evt.Modifiers == key.ModShift {
266 // If they clicked closer to the end, then change the end to
267 // where the caret used to be (effectively swapping start & end).
268 if abs(e.caret.end.ofs-e.caret.start.ofs) < abs(e.caret.start.ofs-prevCaretPos.ofs) {
269 e.caret.end = prevCaretPos
270 }
271 } else {
272 e.ClearSelection()
273 }
274 e.dragging = true
275
276 // Process a double-click.
277 if evt.NumClicks == 2 {
278 e.moveWord(-1, selectionClear)
279 e.moveWord(1, selectionExtend)
280 e.dragging = false
281 }
282 }
283 case pointer.Event:
284 release := false
285 switch {
286 case evt.Type == pointer.Release && evt.Source == pointer.Mouse:
287 release = true
288 fallthrough
289 case evt.Type == pointer.Drag && evt.Source == pointer.Mouse:
290 if e.dragging {
291 e.blinkStart = gtx.Now
292 e.moveCoord(image.Point{
293 X: int(math.Round(float64(evt.Position.X))),
294 Y: int(math.Round(float64(evt.Position.Y))),
295 })
296 e.caret.scroll = true
297
298 if release {
299 e.dragging = false
300 }
301 }
302 }
303 }
304 }
305
306 if (sdist > 0 && soff >= smax) || (sdist < 0 && soff <= smin) {
307 e.scroller.Stop()
308 }
309 }
310
311 func (e *Editor) clickDragEvents(gtx layout.Context) []event.Event {
312 var combinedEvents []event.Event
313 for _, evt := range e.clicker.Events(gtx) {
314 combinedEvents = append(combinedEvents, evt)
315 }
316 for _, evt := range e.dragger.Events(gtx.Metric, gtx, gesture.Both) {
317 combinedEvents = append(combinedEvents, evt)
318 }
319 return combinedEvents
320 }
321
322 func (e *Editor) processKey(gtx layout.Context) {
323 if e.rr.Changed() {
324 e.events = append(e.events, ChangeEvent{})
325 }
326 for _, ke := range gtx.Events(&e.eventKey) {
327 e.blinkStart = gtx.Now
328 switch ke := ke.(type) {
329 case key.FocusEvent:
330 e.focused = ke.Focus
331 case key.Event:
332 if !e.focused || ke.State != key.Press {
333 break
334 }
335 if e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) {
336 if !ke.Modifiers.Contain(key.ModShift) {
337 e.events = append(e.events, SubmitEvent{
338 Text: e.Text(),
339 })
340 continue
341 }
342 }
343 if e.command(gtx, ke) {
344 e.caret.scroll = true
345 e.scroller.Stop()
346 }
347 case key.EditEvent:
348 e.caret.scroll = true
349 e.scroller.Stop()
350 e.append(ke.Text)
351 // Complete a paste event, initiated by Shortcut-V in Editor.command().
352 case clipboard.Event:
353 e.caret.scroll = true
354 e.scroller.Stop()
355 e.append(ke.Text)
356 }
357 if e.rr.Changed() {
358 e.events = append(e.events, ChangeEvent{})
359 }
360 }
361 }
362
363 func (e *Editor) moveLines(distance int, selAct selectionAction) {
364 e.caret.start = e.movePosToLine(e.caret.start, e.caret.start.x+e.caret.start.xoff, e.caret.start.lineCol.Y+distance)
365 e.updateSelection(selAct)
366 }
367
368 func (e *Editor) command(gtx layout.Context, k key.Event) bool {
369 modSkip := key.ModCtrl
370 if runtime.GOOS == "darwin" {
371 modSkip = key.ModAlt
372 }
373 moveByWord := k.Modifiers.Contain(modSkip)
374 selAct := selectionClear
375 if k.Modifiers.Contain(key.ModShift) {
376 selAct = selectionExtend
377 }
378 switch k.Name {
379 case key.NameReturn, key.NameEnter:
380 e.append("\n")
381 case key.NameDeleteBackward:
382 if moveByWord {
383 e.deleteWord(-1)
384 } else {
385 e.Delete(-1)
386 }
387 case key.NameDeleteForward:
388 if moveByWord {
389 e.deleteWord(1)
390 } else {
391 e.Delete(1)
392 }
393 case key.NameUpArrow:
394 e.moveLines(-1, selAct)
395 case key.NameDownArrow:
396 e.moveLines(+1, selAct)
397 case key.NameLeftArrow:
398 if moveByWord {
399 e.moveWord(-1, selAct)
400 } else {
401 if selAct == selectionClear {
402 e.ClearSelection()
403 }
404 e.MoveCaret(-1, -1*int(selAct))
405 }
406 case key.NameRightArrow:
407 if moveByWord {
408 e.moveWord(1, selAct)
409 } else {
410 if selAct == selectionClear {
411 e.ClearSelection()
412 }
413 e.MoveCaret(1, int(selAct))
414 }
415 case key.NamePageUp:
416 e.movePages(-1, selAct)
417 case key.NamePageDown:
418 e.movePages(+1, selAct)
419 case key.NameHome:
420 e.moveStart(selAct)
421 case key.NameEnd:
422 e.moveEnd(selAct)
423 // Initiate a paste operation, by requesting the clipboard contents; other
424 // half is in Editor.processKey() under clipboard.Event.
425 case "V":
426 if k.Modifiers != key.ModShortcut {
427 return false
428 }
429 clipboard.ReadOp{Tag: &e.eventKey}.Add(gtx.Ops)
430 // Copy or Cut selection -- ignored if nothing selected.
431 case "C", "X":
432 if k.Modifiers != key.ModShortcut {
433 return false
434 }
435 if text := e.SelectedText(); text != "" {
436 clipboard.WriteOp{Text: text}.Add(gtx.Ops)
437 if k.Name == "X" {
438 e.Delete(1)
439 }
440 }
441 // Select all
442 case "A":
443 if k.Modifiers != key.ModShortcut {
444 return false
445 }
446 e.caret.end, e.caret.start = e.offsetToScreenPos2(0, e.Len())
447 default:
448 return false
449 }
450 return true
451 }
452
453 // Focus requests the input focus for the Editor.
454 func (e *Editor) Focus() {
455 e.requestFocus = true
456 }
457
458 // Focused returns whether the editor is focused or not.
459 func (e *Editor) Focused() bool {
460 return e.focused
461 }
462
463 // Layout lays out the editor.
464 func (e *Editor) Layout(gtx layout.Context, sh text.Shaper, font text.Font, size unit.Value) layout.Dimensions {
465 textSize := fixed.I(gtx.Px(size))
466 if e.font != font || e.textSize != textSize {
467 e.invalidate()
468 e.font = font
469 e.textSize = textSize
470 }
471 maxWidth := gtx.Constraints.Max.X
472 if e.SingleLine {
473 maxWidth = inf
474 }
475 if maxWidth != e.maxWidth {
476 e.maxWidth = maxWidth
477 e.invalidate()
478 }
479 if sh != e.shaper {
480 e.shaper = sh
481 e.invalidate()
482 }
483 if e.Mask != e.lastMask {
484 e.lastMask = e.Mask
485 e.invalidate()
486 }
487
488 e.makeValid()
489 e.processEvents(gtx)
490 e.makeValid()
491
492 if viewSize := gtx.Constraints.Constrain(e.dims.Size); viewSize != e.viewSize {
493 e.viewSize = viewSize
494 e.invalidate()
495 }
496 e.makeValid()
497
498 return e.layout(gtx)
499 }
500
501 func (e *Editor) layout(gtx layout.Context) layout.Dimensions {
502 // Adjust scrolling for new viewport and layout.
503 e.scrollRel(0, 0)
504
505 if e.caret.scroll {
506 e.caret.scroll = false
507 e.scrollToCaret()
508 }
509
510 off := image.Point{
511 X: -e.scrollOff.X,
512 Y: -e.scrollOff.Y,
513 }
514 clip := textPadding(e.lines)
515 clip.Max = clip.Max.Add(e.viewSize)
516 startSel, endSel := sortPoints(e.caret.start.lineCol, e.caret.end.lineCol)
517 it := segmentIterator{
518 startSel: startSel,
519 endSel: endSel,
520 Lines: e.lines,
521 Clip: clip,
522 Alignment: e.Alignment,
523 Width: e.viewSize.X,
524 Offset: off,
525 }
526 e.shapes = e.shapes[:0]
527 for {
528 layout, off, selected, yOffs, size, ok := it.Next()
529 if !ok {
530 break
531 }
532 path := e.shaper.Shape(e.font, e.textSize, layout)
533 e.shapes = append(e.shapes, line{off, path, selected, yOffs, size})
534 }
535
536 key.InputOp{Tag: &e.eventKey}.Add(gtx.Ops)
537 if e.requestFocus {
538 key.FocusOp{Tag: &e.eventKey}.Add(gtx.Ops)
539 key.SoftKeyboardOp{Show: true}.Add(gtx.Ops)
540 }
541 e.requestFocus = false
542 pointerPadding := gtx.Px(unit.Dp(4))
543 r := image.Rectangle{Max: e.viewSize}
544 r.Min.X -= pointerPadding
545 r.Min.Y -= pointerPadding
546 r.Max.X += pointerPadding
547 r.Max.X += pointerPadding
548 pointer.Rect(r).Add(gtx.Ops)
549 pointer.CursorNameOp{Name: pointer.CursorText}.Add(gtx.Ops)
550
551 var scrollRange image.Rectangle
552 if e.SingleLine {
553 scrollRange.Min.X = -e.scrollOff.X
554 scrollRange.Max.X = max(0, e.dims.Size.X-(e.scrollOff.X+e.viewSize.X))
555 } else {
556 scrollRange.Min.Y = -e.scrollOff.Y
557 scrollRange.Max.Y = max(0, e.dims.Size.Y-(e.scrollOff.Y+e.viewSize.Y))
558 }
559 e.scroller.Add(gtx.Ops, scrollRange)
560
561 e.clicker.Add(gtx.Ops)
562 e.dragger.Add(gtx.Ops)
563 e.caret.on = false
564 if e.focused {
565 now := gtx.Now
566 dt := now.Sub(e.blinkStart)
567 blinking := dt < maxBlinkDuration
568 const timePerBlink = time.Second / blinksPerSecond
569 nextBlink := now.Add(timePerBlink/2 - dt%(timePerBlink/2))
570 if blinking {
571 redraw := op.InvalidateOp{At: nextBlink}
572 redraw.Add(gtx.Ops)
573 }
574 e.caret.on = e.focused && (!blinking || dt%timePerBlink < timePerBlink/2)
575 }
576
577 return layout.Dimensions{Size: e.viewSize, Baseline: e.dims.Baseline}
578 }
579
580 // PaintSelection paints the contrasting background for selected text.
581 func (e *Editor) PaintSelection(gtx layout.Context) {
582 cl := textPadding(e.lines)
583 cl.Max = cl.Max.Add(e.viewSize)
584 clip.Rect(cl).Add(gtx.Ops)
585 for _, shape := range e.shapes {
586 if !shape.selected {
587 continue
588 }
589 stack := op.Save(gtx.Ops)
590 offset := shape.offset
591 offset.Y += shape.selectionYOffs
592 op.Offset(layout.FPt(offset)).Add(gtx.Ops)
593 clip.Rect(image.Rectangle{Max: shape.selectionSize}).Add(gtx.Ops)
594 paint.PaintOp{}.Add(gtx.Ops)
595 stack.Load()
596 }
597 }
598
599 func (e *Editor) PaintText(gtx layout.Context) {
600 cl := textPadding(e.lines)
601 cl.Max = cl.Max.Add(e.viewSize)
602 clip.Rect(cl).Add(gtx.Ops)
603 for _, shape := range e.shapes {
604 stack := op.Save(gtx.Ops)
605 op.Offset(layout.FPt(shape.offset)).Add(gtx.Ops)
606 shape.clip.Add(gtx.Ops)
607 paint.PaintOp{}.Add(gtx.Ops)
608 stack.Load()
609 }
610 }
611
612 func (e *Editor) PaintCaret(gtx layout.Context) {
613 if !e.caret.on {
614 return
615 }
616 e.makeValid()
617 carWidth := fixed.I(gtx.Px(unit.Dp(1)))
618 carX := e.caret.start.x
619 carY := e.caret.start.y
620
621 defer op.Save(gtx.Ops).Load()
622 carX -= carWidth / 2
623 carAsc, carDesc := -e.lines[e.caret.start.lineCol.Y].Bounds.Min.Y, e.lines[e.caret.start.lineCol.Y].Bounds.Max.Y
624 carRect := image.Rectangle{
625 Min: image.Point{X: carX.Ceil(), Y: carY - carAsc.Ceil()},
626 Max: image.Point{X: carX.Ceil() + carWidth.Ceil(), Y: carY + carDesc.Ceil()},
627 }
628 carRect = carRect.Add(image.Point{
629 X: -e.scrollOff.X,
630 Y: -e.scrollOff.Y,
631 })
632 cl := textPadding(e.lines)
633 // Account for caret width to each side.
634 whalf := (carWidth / 2).Ceil()
635 if cl.Max.X < whalf {
636 cl.Max.X = whalf
637 }
638 if cl.Min.X > -whalf {
639 cl.Min.X = -whalf
640 }
641 cl.Max = cl.Max.Add(e.viewSize)
642 carRect = cl.Intersect(carRect)
643 if !carRect.Empty() {
644 st := op.Save(gtx.Ops)
645 clip.Rect(carRect).Add(gtx.Ops)
646 paint.PaintOp{}.Add(gtx.Ops)
647 st.Load()
648 }
649 }
650
651 // Len is the length of the editor contents.
652 func (e *Editor) Len() int {
653 return e.rr.len()
654 }
655
656 // Text returns the contents of the editor.
657 func (e *Editor) Text() string {
658 return e.rr.String()
659 }
660
661 // SetText replaces the contents of the editor, clearing any selection first.
662 func (e *Editor) SetText(s string) {
663 e.rr = editBuffer{}
664 e.caret.start = combinedPos{}
665 e.caret.end = combinedPos{}
666 e.prepend(s)
667 }
668
669 func (e *Editor) scrollBounds() image.Rectangle {
670 var b image.Rectangle
671 if e.SingleLine {
672 if len(e.lines) > 0 {
673 b.Min.X = align(e.Alignment, e.lines[0].Width, e.viewSize.X).Floor()
674 if b.Min.X > 0 {
675 b.Min.X = 0
676 }
677 }
678 b.Max.X = e.dims.Size.X + b.Min.X - e.viewSize.X
679 } else {
680 b.Max.Y = e.dims.Size.Y - e.viewSize.Y
681 }
682 return b
683 }
684
685 func (e *Editor) scrollRel(dx, dy int) {
686 e.scrollAbs(e.scrollOff.X+dx, e.scrollOff.Y+dy)
687 }
688
689 func (e *Editor) scrollAbs(x, y int) {
690 e.scrollOff.X = x
691 e.scrollOff.Y = y
692 b := e.scrollBounds()
693 if e.scrollOff.X > b.Max.X {
694 e.scrollOff.X = b.Max.X
695 }
696 if e.scrollOff.X < b.Min.X {
697 e.scrollOff.X = b.Min.X
698 }
699 if e.scrollOff.Y > b.Max.Y {
700 e.scrollOff.Y = b.Max.Y
701 }
702 if e.scrollOff.Y < b.Min.Y {
703 e.scrollOff.Y = b.Min.Y
704 }
705 }
706
707 func (e *Editor) moveCoord(pos image.Point) {
708 var (
709 prevDesc fixed.Int26_6
710 carLine int
711 y int
712 )
713 for _, l := range e.lines {
714 y += (prevDesc + l.Ascent).Ceil()
715 prevDesc = l.Descent
716 if y+prevDesc.Ceil() >= pos.Y+e.scrollOff.Y {
717 break
718 }
719 carLine++
720 }
721 x := fixed.I(pos.X + e.scrollOff.X)
722 e.caret.start = e.movePosToLine(e.caret.start, x, carLine)
723 e.caret.start.xoff = 0
724 }
725
726 func (e *Editor) layoutText(s text.Shaper) ([]text.Line, layout.Dimensions) {
727 e.rr.Reset()
728 var r io.Reader = &e.rr
729 if e.Mask != 0 {
730 e.maskReader.Reset(&e.rr, e.Mask)
731 r = &e.maskReader
732 }
733 var lines []text.Line
734 if s != nil {
735 lines, _ = s.Layout(e.font, e.textSize, e.maxWidth, r)
736 } else {
737 lines, _ = nullLayout(r)
738 }
739 dims := linesDimens(lines)
740 for i := 0; i < len(lines)-1; i++ {
741 // To avoid layout flickering while editing, assume a soft newline takes
742 // up all available space.
743 if layout := lines[i].Layout; len(layout.Text) > 0 {
744 r := layout.Text[len(layout.Text)-1]
745 if r != '\n' {
746 dims.Size.X = e.maxWidth
747 break
748 }
749 }
750 }
751 return lines, dims
752 }
753
754 // CaretPos returns the line & column numbers of the caret.
755 func (e *Editor) CaretPos() (line, col int) {
756 e.makeValid()
757 return e.caret.start.lineCol.Y, e.caret.start.lineCol.X
758 }
759
760 // CaretCoords returns the coordinates of the caret, relative to the
761 // editor itself.
762 func (e *Editor) CaretCoords() f32.Point {
763 e.makeValid()
764 return f32.Pt(float32(e.caret.start.x)/64, float32(e.caret.start.y))
765 }
766
767 // offsetToScreenPos2 is a utility function to shortcut the common case of
768 // wanting the positions of exactly two offsets.
769 func (e *Editor) offsetToScreenPos2(o1, o2 int) (combinedPos, combinedPos) {
770 cp1, iter := e.offsetToScreenPos(o1)
771 return cp1, iter(o2)
772 }
773
774 // offsetToScreenPos takes an offset into the editor text (e.g.
775 // e.caret.end.ofs) and returns a combinedPos that corresponds to its current
776 // screen position, as well as an iterator that lets you get the combinedPos
777 // of a later offset. The offsets given to offsetToScreenPos and to the
778 // returned iterator must be sorted, lowest first, and they must be valid (0
779 // <= offset <= e.Len()).
780 //
781 // This function is written this way to take advantage of previous work done
782 // for offsets after the first. Otherwise you have to start from the top each
783 // time.
784 func (e *Editor) offsetToScreenPos(offset int) (combinedPos, func(int) combinedPos) {
785 var col, line, idx int
786 var x fixed.Int26_6
787
788 l := e.lines[line]
789 y := l.Ascent.Ceil()
790 prevDesc := l.Descent
791
792 iter := func(offset int) combinedPos {
793 LOOP:
794 for {
795 for ; col < len(l.Layout.Advances); col++ {
796 if idx >= offset {
797 break LOOP
798 }
799
800 x += l.Layout.Advances[col]
801 _, s := e.rr.runeAt(idx)
802 idx += s
803 }
804 if lastLine := line == len(e.lines)-1; lastLine || idx > offset {
805 break LOOP
806 }
807
808 line++
809 x = 0
810 col = 0
811 l = e.lines[line]
812 y += (prevDesc + l.Ascent).Ceil()
813 prevDesc = l.Descent
814 }
815 return combinedPos{
816 lineCol: screenPos{Y: line, X: col},
817 x: x + align(e.Alignment, e.lines[line].Width, e.viewSize.X),
818 y: y,
819 ofs: offset,
820 }
821 }
822 return iter(offset), iter
823 }
824
825 func (e *Editor) invalidate() {
826 e.valid = false
827 }
828
829 // Delete runes from the caret position. The sign of runes specifies the
830 // direction to delete: positive is forward, negative is backward.
831 //
832 // If there is a selection, it is deleted and counts as a single rune.
833 func (e *Editor) Delete(runes int) {
834 if runes == 0 {
835 return
836 }
837
838 if l := e.caret.end.ofs - e.caret.start.ofs; l != 0 {
839 e.caret.start.ofs = e.rr.deleteRunes(e.caret.start.ofs, l)
840 runes -= sign(runes)
841 }
842
843 e.caret.start.ofs = e.rr.deleteRunes(e.caret.start.ofs, runes)
844 e.caret.start.xoff = 0
845 e.ClearSelection()
846 e.invalidate()
847 }
848
849 // Insert inserts text at the caret, moving the caret forward. If there is a
850 // selection, Insert overwrites it.
851 func (e *Editor) Insert(s string) {
852 e.append(s)
853 e.caret.scroll = true
854 }
855
856 // append inserts s at the cursor, leaving the caret is at the end of s. If
857 // there is a selection, append overwrites it.
858 // xxx|yyy + append zzz => xxxzzz|yyy
859 func (e *Editor) append(s string) {
860 e.prepend(s)
861 e.caret.start.ofs += len(s)
862 e.caret.end.ofs = e.caret.start.ofs
863 }
864
865 // prepend inserts s after the cursor; the caret does not change. If there is
866 // a selection, prepend overwrites it.
867 // xxx|yyy + prepend zzz => xxx|zzzyyy
868 func (e *Editor) prepend(s string) {
869 if e.SingleLine {
870 s = strings.ReplaceAll(s, "\n", " ")
871 }
872 e.caret.start.ofs = e.rr.deleteRunes(e.caret.start.ofs, e.caret.end.ofs-e.caret.start.ofs) // Delete any selection first.
873 e.rr.prepend(e.caret.start.ofs, s)
874 e.caret.start.xoff = 0
875 e.invalidate()
876 }
877
878 func (e *Editor) movePages(pages int, selAct selectionAction) {
879 e.makeValid()
880 y := e.caret.start.y + pages*e.viewSize.Y
881 var (
882 prevDesc fixed.Int26_6
883 carLine2 int
884 )
885 y2 := e.lines[0].Ascent.Ceil()
886 for i := 1; i < len(e.lines); i++ {
887 if y2 >= y {
888 break
889 }
890 l := e.lines[i]
891 h := (prevDesc + l.Ascent).Ceil()
892 prevDesc = l.Descent
893 if y2+h-y >= y-y2 {
894 break
895 }
896 y2 += h
897 carLine2++
898 }
899 e.caret.start = e.movePosToLine(e.caret.start, e.caret.start.x+e.caret.start.xoff, carLine2)
900 e.updateSelection(selAct)
901 }
902
903 func (e *Editor) movePosToLine(pos combinedPos, x fixed.Int26_6, line int) combinedPos {
904 e.makeValid(&pos)
905 if line < 0 {
906 line = 0
907 }
908 if line >= len(e.lines) {
909 line = len(e.lines) - 1
910 }
911
912 prevDesc := e.lines[line].Descent
913 for pos.lineCol.Y < line {
914 pos = e.movePosToEnd(pos)
915 l := e.lines[pos.lineCol.Y]
916 _, s := e.rr.runeAt(pos.ofs)
917 pos.ofs += s
918 pos.y += (prevDesc + l.Ascent).Ceil()
919 pos.lineCol.X = 0
920 prevDesc = l.Descent
921 pos.lineCol.Y++
922 }
923 for pos.lineCol.Y > line {
924 pos = e.movePosToStart(pos)
925 l := e.lines[pos.lineCol.Y]
926 _, s := e.rr.runeBefore(pos.ofs)
927 pos.ofs -= s
928 pos.y -= (prevDesc + l.Ascent).Ceil()
929 prevDesc = l.Descent
930 pos.lineCol.Y--
931 l = e.lines[pos.lineCol.Y]
932 pos.lineCol.X = len(l.Layout.Advances) - 1
933 }
934
935 pos = e.movePosToStart(pos)
936 l := e.lines[line]
937 pos.x = align(e.Alignment, l.Width, e.viewSize.X)
938 // Only move past the end of the last line
939 end := 0
940 if line < len(e.lines)-1 {
941 end = 1
942 }
943 // Move to rune closest to x.
944 for i := 0; i < len(l.Layout.Advances)-end; i++ {
945 adv := l.Layout.Advances[i]
946 if pos.x >= x {
947 break
948 }
949 if pos.x+adv-x >= x-pos.x {
950 break
951 }
952 pos.x += adv
953 _, s := e.rr.runeAt(pos.ofs)
954 pos.ofs += s
955 pos.lineCol.X++
956 }
957 pos.xoff = x - pos.x
958 return pos
959 }
960
961 // MoveCaret moves the caret (aka selection start) and the selection end
962 // relative to their current positions. Positive distances moves forward,
963 // negative distances moves backward. Distances are in runes.
964 func (e *Editor) MoveCaret(startDelta, endDelta int) {
965 e.makeValid()
966 keepSame := e.caret.start.ofs == e.caret.end.ofs && startDelta == endDelta
967 e.caret.start = e.movePos(e.caret.start, startDelta)
968 e.caret.start.xoff = 0
969 // If they were in the same place, and we're moving them the same distance,
970 // just assign the new position, instead of recalculating it.
971 if keepSame {
972 e.caret.end = e.caret.start
973 } else {
974 e.caret.end = e.movePos(e.caret.end, endDelta)
975 e.caret.end.xoff = 0
976 }
977 }
978
979 func (e *Editor) movePos(pos combinedPos, distance int) combinedPos {
980 for ; distance < 0 && pos.ofs > 0; distance++ {
981 if pos.lineCol.X == 0 {
982 // Move to end of previous line.
983 pos = e.movePosToLine(pos, fixed.I(e.maxWidth), pos.lineCol.Y-1)
984 continue
985 }
986 l := e.lines[pos.lineCol.Y].Layout
987 _, s := e.rr.runeBefore(pos.ofs)
988 pos.ofs -= s
989 pos.lineCol.X--
990 pos.x -= l.Advances[pos.lineCol.X]
991 }
992 for ; distance > 0 && pos.ofs < e.rr.len(); distance-- {
993 l := e.lines[pos.lineCol.Y].Layout
994 // Only move past the end of the last line
995 end := 0
996 if pos.lineCol.Y < len(e.lines)-1 {
997 end = 1
998 }
999 if pos.lineCol.X >= len(l.Advances)-end {
1000 // Move to start of next line.
1001 pos = e.movePosToLine(pos, 0, pos.lineCol.Y+1)
1002 continue
1003 }
1004 pos.x += l.Advances[pos.lineCol.X]
1005 _, s := e.rr.runeAt(pos.ofs)
1006 pos.ofs += s
1007 pos.lineCol.X++
1008 }
1009 return pos
1010 }
1011
1012 func (e *Editor) moveStart(selAct selectionAction) {
1013 e.caret.start = e.movePosToStart(e.caret.start)
1014 e.updateSelection(selAct)
1015 }
1016
1017 func (e *Editor) movePosToStart(pos combinedPos) combinedPos {
1018 e.makeValid(&pos)
1019 layout := e.lines[pos.lineCol.Y].Layout
1020 for i := pos.lineCol.X - 1; i >= 0; i-- {
1021 _, s := e.rr.runeBefore(pos.ofs)
1022 pos.ofs -= s
1023 pos.x -= layout.Advances[i]
1024 }
1025 pos.lineCol.X = 0
1026 pos.xoff = -pos.x
1027 return pos
1028 }
1029
1030 func (e *Editor) moveEnd(selAct selectionAction) {
1031 e.caret.start = e.movePosToEnd(e.caret.start)
1032 e.updateSelection(selAct)
1033 }
1034
1035 func (e *Editor) movePosToEnd(pos combinedPos) combinedPos {
1036 e.makeValid(&pos)
1037 l := e.lines[pos.lineCol.Y]
1038 // Only move past the end of the last line
1039 end := 0
1040 if pos.lineCol.Y < len(e.lines)-1 {
1041 end = 1
1042 }
1043 layout := l.Layout
1044 for i := pos.lineCol.X; i < len(layout.Advances)-end; i++ {
1045 adv := layout.Advances[i]
1046 _, s := e.rr.runeAt(pos.ofs)
1047 pos.ofs += s
1048 pos.x += adv
1049 pos.lineCol.X++
1050 }
1051 a := align(e.Alignment, l.Width, e.viewSize.X)
1052 pos.xoff = l.Width + a - pos.x
1053 return pos
1054 }
1055
1056 // moveWord moves the caret to the next word in the specified direction.
1057 // Positive is forward, negative is backward.
1058 // Absolute values greater than one will skip that many words.
1059 func (e *Editor) moveWord(distance int, selAct selectionAction) {
1060 e.makeValid()
1061 // split the distance information into constituent parts to be
1062 // used independently.
1063 words, direction := distance, 1
1064 if distance < 0 {
1065 words, direction = distance*-1, -1
1066 }
1067 // atEnd if caret is at either side of the buffer.
1068 atEnd := func() bool {
1069 return e.caret.start.ofs == 0 || e.caret.start.ofs == e.rr.len()
1070 }
1071 // next returns the appropriate rune given the direction.
1072 next := func() (r rune) {
1073 if direction < 0 {
1074 r, _ = e.rr.runeBefore(e.caret.start.ofs)
1075 } else {
1076 r, _ = e.rr.runeAt(e.caret.start.ofs)
1077 }
1078 return r
1079 }
1080 for ii := 0; ii < words; ii++ {
1081 for r := next(); unicode.IsSpace(r) && !atEnd(); r = next() {
1082 e.MoveCaret(direction, 0)
1083 }
1084 e.MoveCaret(direction, 0)
1085 for r := next(); !unicode.IsSpace(r) && !atEnd(); r = next() {
1086 e.MoveCaret(direction, 0)
1087 }
1088 }
1089 e.updateSelection(selAct)
1090 }
1091
1092 // deleteWord deletes the next word(s) in the specified direction.
1093 // Unlike moveWord, deleteWord treats whitespace as a word itself.
1094 // Positive is forward, negative is backward.
1095 // Absolute values greater than one will delete that many words.
1096 // The selection counts as a single word.
1097 func (e *Editor) deleteWord(distance int) {
1098 if distance == 0 {
1099 return
1100 }
1101
1102 e.makeValid()
1103
1104 if e.caret.start.ofs != e.caret.end.ofs {
1105 e.Delete(1)
1106 distance -= sign(distance)
1107 }
1108 if distance == 0 {
1109 return
1110 }
1111
1112 // split the distance information into constituent parts to be
1113 // used independently.
1114 words, direction := distance, 1
1115 if distance < 0 {
1116 words, direction = distance*-1, -1
1117 }
1118 // atEnd if offset is at or beyond either side of the buffer.
1119 atEnd := func(offset int) bool {
1120 idx := e.caret.start.ofs + offset*direction
1121 return idx <= 0 || idx >= e.rr.len()
1122 }
1123 // next returns the appropriate rune given the direction and offset.
1124 next := func(offset int) (r rune) {
1125 idx := e.caret.start.ofs + offset*direction
1126 if idx < 0 {
1127 idx = 0
1128 } else if idx > e.rr.len() {
1129 idx = e.rr.len()
1130 }
1131 if direction < 0 {
1132 r, _ = e.rr.runeBefore(idx)
1133 } else {
1134 r, _ = e.rr.runeAt(idx)
1135 }
1136 return r
1137 }
1138 var runes = 1
1139 for ii := 0; ii < words; ii++ {
1140 if r := next(runes); unicode.IsSpace(r) {
1141 for r := next(runes); unicode.IsSpace(r) && !atEnd(runes); r = next(runes) {
1142 runes += 1
1143 }
1144 } else {
1145 for r := next(runes); !unicode.IsSpace(r) && !atEnd(runes); r = next(runes) {
1146 runes += 1
1147 }
1148 }
1149 }
1150 e.Delete(runes * direction)
1151 }
1152
1153 func (e *Editor) scrollToCaret() {
1154 e.makeValid()
1155 l := e.lines[e.caret.start.lineCol.Y]
1156 if e.SingleLine {
1157 var dist int
1158 if d := e.caret.start.x.Floor() - e.scrollOff.X; d < 0 {
1159 dist = d
1160 } else if d := e.caret.start.x.Ceil() - (e.scrollOff.X + e.viewSize.X); d > 0 {
1161 dist = d
1162 }
1163 e.scrollRel(dist, 0)
1164 } else {
1165 miny := e.caret.start.y - l.Ascent.Ceil()
1166 maxy := e.caret.start.y + l.Descent.Ceil()
1167 var dist int
1168 if d := miny - e.scrollOff.Y; d < 0 {
1169 dist = d
1170 } else if d := maxy - (e.scrollOff.Y + e.viewSize.Y); d > 0 {
1171 dist = d
1172 }
1173 e.scrollRel(0, dist)
1174 }
1175 }
1176
1177 // NumLines returns the number of lines in the editor.
1178 func (e *Editor) NumLines() int {
1179 e.makeValid()
1180 return len(e.lines)
1181 }
1182
1183 // SelectionLen returns the length of the selection, in bytes; it is
1184 // equivalent to len(e.SelectedText()).
1185 func (e *Editor) SelectionLen() int {
1186 return abs(e.caret.start.ofs - e.caret.end.ofs)
1187 }
1188
1189 // Selection returns the start and end of the selection, as offsets into the
1190 // editor text. start can be > end.
1191 func (e *Editor) Selection() (start, end int) {
1192 return e.caret.start.ofs, e.caret.end.ofs
1193 }
1194
1195 // SetCaret moves the caret to start, and sets the selection end to end. start
1196 // and end are in bytes, and represent offsets into the editor text. start and
1197 // end must be at a rune boundary.
1198 func (e *Editor) SetCaret(start, end int) {
1199 e.makeValid()
1200 // Constrain start and end to [0, e.Len()].
1201 l := e.Len()
1202 start = max(min(start, l), 0)
1203 end = max(min(end, l), 0)
1204 e.caret.start.ofs, e.caret.end.ofs = start, end
1205 e.makeValidCaret()
1206 e.caret.scroll = true
1207 e.scroller.Stop()
1208 }
1209
1210 func (e *Editor) makeValidCaret(positions ...*combinedPos) {
1211 // Jump through some hoops to order the offsets given to offsetToScreenPos,
1212 // but still be able to update them correctly with the results thereof.
1213 positions = append(positions, &e.caret.start, &e.caret.end)
1214 sort.Slice(positions, func(i, j int) bool {
1215 return positions[i].ofs < positions[j].ofs
1216 })
1217 var iter func(offset int) combinedPos
1218 *positions[0], iter = e.offsetToScreenPos(positions[0].ofs)
1219 for _, cp := range positions[1:] {
1220 *cp = iter(cp.ofs)
1221 }
1222 }
1223
1224 // SelectedText returns the currently selected text (if any) from the editor.
1225 func (e *Editor) SelectedText() string {
1226 l := e.SelectionLen()
1227 if l == 0 {
1228 return ""
1229 }
1230 buf := make([]byte, l)
1231 e.rr.Seek(int64(min(e.caret.start.ofs, e.caret.end.ofs)), io.SeekStart)
1232 _, err := e.rr.Read(buf)
1233 if err != nil {
1234 // The only error that rr.Read can return is EOF, which just means no
1235 // selection, but we've already made sure that shouldn't happen.
1236 panic("impossible error because end is before e.rr.Len()")
1237 }
1238 return string(buf)
1239 }
1240
1241 func (e *Editor) updateSelection(selAct selectionAction) {
1242 if selAct == selectionClear {
1243 e.ClearSelection()
1244 }
1245 }
1246
1247 // ClearSelection clears the selection, by setting the selection end equal to
1248 // the selection start.
1249 func (e *Editor) ClearSelection() {
1250 e.caret.end = e.caret.start
1251 }
1252
1253 func max(a, b int) int {
1254 if a > b {
1255 return a
1256 }
1257 return b
1258 }
1259
1260 func min(a, b int) int {
1261 if a < b {
1262 return a
1263 }
1264 return b
1265 }
1266
1267 func abs(n int) int {
1268 if n < 0 {
1269 return -n
1270 }
1271 return n
1272 }
1273
1274 func sign(n int) int {
1275 switch {
1276 case n < 0:
1277 return -1
1278 case n > 0:
1279 return 1
1280 default:
1281 return 0
1282 }
1283 }
1284
1285 // sortPoints returns a and b sorted such that a2 <= b2.
1286 func sortPoints(a, b screenPos) (a2, b2 screenPos) {
1287 if b.Less(a) {
1288 return b, a
1289 }
1290 return a, b
1291 }
1292
1293 func nullLayout(r io.Reader) ([]text.Line, error) {
1294 rr := bufio.NewReader(r)
1295 var rerr error
1296 var n int
1297 var buf bytes.Buffer
1298 for {
1299 r, s, err := rr.ReadRune()
1300 n += s
1301 buf.WriteRune(r)
1302 if err != nil {
1303 rerr = err
1304 break
1305 }
1306 }
1307 return []text.Line{
1308 {
1309 Layout: text.Layout{
1310 Text: buf.String(),
1311 Advances: make([]fixed.Int26_6, n),
1312 },
1313 },
1314 }, rerr
1315 }
1316
1317 func (s ChangeEvent) isEditorEvent() {}
1318 func (s SubmitEvent) isEditorEvent() {}
1319 func (s SelectEvent) isEditorEvent() {}
1320