12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130 |
- // SPDX-License-Identifier: Unlicense OR MIT
- package widget
- import (
- "bufio"
- "image"
- "io"
- "math"
- "strings"
- "time"
- "unicode"
- "unicode/utf8"
- "gioui.org/f32"
- "gioui.org/font"
- "gioui.org/gesture"
- "gioui.org/io/clipboard"
- "gioui.org/io/event"
- "gioui.org/io/key"
- "gioui.org/io/pointer"
- "gioui.org/io/semantic"
- "gioui.org/io/system"
- "gioui.org/io/transfer"
- "gioui.org/layout"
- "gioui.org/op"
- "gioui.org/op/clip"
- "gioui.org/text"
- "gioui.org/unit"
- )
- // Editor implements an editable and scrollable text area.
- type Editor struct {
- // text manages the text buffer and provides shaping and cursor positioning
- // services.
- text textView
- // Alignment controls the alignment of text within the editor.
- Alignment text.Alignment
- // LineHeight determines the gap between baselines of text. If zero, a sensible
- // default will be used.
- LineHeight unit.Sp
- // LineHeightScale is multiplied by LineHeight to determine the final gap
- // between baselines. If zero, a sensible default will be used.
- LineHeightScale float32
- // SingleLine force the text to stay on a single line.
- // SingleLine also sets the scrolling direction to
- // horizontal.
- SingleLine bool
- // ReadOnly controls whether the contents of the editor can be altered by
- // user interaction. If set to true, the editor will allow selecting text
- // and copying it interactively, but not modifying it.
- ReadOnly bool
- // Submit enabled translation of carriage return keys to SubmitEvents.
- // If not enabled, carriage returns are inserted as newlines in the text.
- Submit bool
- // Mask replaces the visual display of each rune in the contents with the given rune.
- // Newline characters are not masked. When non-zero, the unmasked contents
- // are accessed by Len, Text, and SetText.
- Mask rune
- // InputHint specifies the type of on-screen keyboard to be displayed.
- InputHint key.InputHint
- // MaxLen limits the editor content to a maximum length. Zero means no limit.
- MaxLen int
- // Filter is the list of characters allowed in the Editor. If Filter is empty,
- // all characters are allowed.
- Filter string
- // WrapPolicy configures how displayed text will be broken into lines.
- WrapPolicy text.WrapPolicy
- buffer *editBuffer
- // scratch is a byte buffer that is reused to efficiently read portions of text
- // from the textView.
- scratch []byte
- blinkStart time.Time
- // ime tracks the state relevant to input methods.
- ime struct {
- imeState
- scratch []byte
- }
- dragging bool
- dragger gesture.Drag
- scroller gesture.Scroll
- scrollCaret bool
- showCaret bool
- clicker gesture.Click
- // history contains undo history.
- history []modification
- // nextHistoryIdx is the index within the history of the next modification. This
- // is only not len(history) immediately after undo operations occur. It is framed as the "next" value
- // to make the zero value consistent.
- nextHistoryIdx int
- pending []EditorEvent
- }
- type offEntry struct {
- runes int
- bytes int
- }
- type imeState struct {
- selection struct {
- rng key.Range
- caret key.Caret
- }
- snippet key.Snippet
- start, end int
- }
- type maskReader struct {
- // rr is the underlying reader.
- rr io.RuneReader
- maskBuf [utf8.UTFMax]byte
- // mask is the utf-8 encoded mask rune.
- mask []byte
- // overflow contains excess mask bytes left over after the last Read call.
- overflow []byte
- }
- type selectionAction int
- const (
- selectionExtend selectionAction = iota
- selectionClear
- )
- func (m *maskReader) Reset(r io.Reader, mr rune) {
- m.rr = bufio.NewReader(r)
- n := utf8.EncodeRune(m.maskBuf[:], mr)
- m.mask = m.maskBuf[:n]
- }
- // Read reads from the underlying reader and replaces every
- // rune with the mask rune.
- func (m *maskReader) Read(b []byte) (n int, err error) {
- for len(b) > 0 {
- var replacement []byte
- if len(m.overflow) > 0 {
- replacement = m.overflow
- } else {
- var r rune
- r, _, err = m.rr.ReadRune()
- if err != nil {
- break
- }
- if r == '\n' {
- replacement = []byte{'\n'}
- } else {
- replacement = m.mask
- }
- }
- nn := copy(b, replacement)
- m.overflow = replacement[nn:]
- n += nn
- b = b[nn:]
- }
- return n, err
- }
- type EditorEvent interface {
- isEditorEvent()
- }
- // A ChangeEvent is generated for every user change to the text.
- type ChangeEvent struct{}
- // A SubmitEvent is generated when Submit is set
- // and a carriage return key is pressed.
- type SubmitEvent struct {
- Text string
- }
- // A SelectEvent is generated when the user selects some text, or changes the
- // selection (e.g. with a shift-click), including if they remove the
- // selection. The selected text is not part of the event, on the theory that
- // it could be a relatively expensive operation (for a large editor), most
- // applications won't actually care about it, and those that do can call
- // Editor.SelectedText() (which can be empty).
- type SelectEvent struct{}
- const (
- blinksPerSecond = 1
- maxBlinkDuration = 10 * time.Second
- )
- func (e *Editor) processEvents(gtx layout.Context) (ev EditorEvent, ok bool) {
- if len(e.pending) > 0 {
- out := e.pending[0]
- e.pending = e.pending[:copy(e.pending, e.pending[1:])]
- return out, true
- }
- selStart, selEnd := e.Selection()
- defer func() {
- afterSelStart, afterSelEnd := e.Selection()
- if selStart != afterSelStart || selEnd != afterSelEnd {
- if ok {
- e.pending = append(e.pending, SelectEvent{})
- } else {
- ev = SelectEvent{}
- ok = true
- }
- }
- }()
- ev, ok = e.processPointer(gtx)
- if ok {
- return ev, ok
- }
- ev, ok = e.processKey(gtx)
- if ok {
- return ev, ok
- }
- return nil, false
- }
- func (e *Editor) processPointer(gtx layout.Context) (EditorEvent, bool) {
- sbounds := e.text.ScrollBounds()
- var smin, smax int
- var axis gesture.Axis
- if e.SingleLine {
- axis = gesture.Horizontal
- smin, smax = sbounds.Min.X, sbounds.Max.X
- } else {
- axis = gesture.Vertical
- smin, smax = sbounds.Min.Y, sbounds.Max.Y
- }
- var scrollX, scrollY pointer.ScrollRange
- textDims := e.text.FullDimensions()
- visibleDims := e.text.Dimensions()
- if e.SingleLine {
- scrollOffX := e.text.ScrollOff().X
- scrollX.Min = min(-scrollOffX, 0)
- scrollX.Max = max(0, textDims.Size.X-(scrollOffX+visibleDims.Size.X))
- } else {
- scrollOffY := e.text.ScrollOff().Y
- scrollY.Min = -scrollOffY
- scrollY.Max = max(0, textDims.Size.Y-(scrollOffY+visibleDims.Size.Y))
- }
- sdist := e.scroller.Update(gtx.Metric, gtx.Source, gtx.Now, axis, scrollX, scrollY)
- var soff int
- if e.SingleLine {
- e.text.ScrollRel(sdist, 0)
- soff = e.text.ScrollOff().X
- } else {
- e.text.ScrollRel(0, sdist)
- soff = e.text.ScrollOff().Y
- }
- for {
- evt, ok := e.clicker.Update(gtx.Source)
- if !ok {
- break
- }
- ev, ok := e.processPointerEvent(gtx, evt)
- if ok {
- return ev, ok
- }
- }
- for {
- evt, ok := e.dragger.Update(gtx.Metric, gtx.Source, gesture.Both)
- if !ok {
- break
- }
- ev, ok := e.processPointerEvent(gtx, evt)
- if ok {
- return ev, ok
- }
- }
- if (sdist > 0 && soff >= smax) || (sdist < 0 && soff <= smin) {
- e.scroller.Stop()
- }
- return nil, false
- }
- func (e *Editor) processPointerEvent(gtx layout.Context, ev event.Event) (EditorEvent, bool) {
- switch evt := ev.(type) {
- case gesture.ClickEvent:
- switch {
- case evt.Kind == gesture.KindPress && evt.Source == pointer.Mouse,
- evt.Kind == gesture.KindClick && evt.Source != pointer.Mouse:
- prevCaretPos, _ := e.text.Selection()
- e.blinkStart = gtx.Now
- e.text.MoveCoord(image.Point{
- X: int(math.Round(float64(evt.Position.X))),
- Y: int(math.Round(float64(evt.Position.Y))),
- })
- gtx.Execute(key.FocusCmd{Tag: e})
- if !e.ReadOnly {
- gtx.Execute(key.SoftKeyboardCmd{Show: true})
- }
- if e.scroller.State() != gesture.StateFlinging {
- e.scrollCaret = true
- }
- if evt.Modifiers == key.ModShift {
- start, end := e.text.Selection()
- // If they clicked closer to the end, then change the end to
- // where the caret used to be (effectively swapping start & end).
- if abs(end-start) < abs(start-prevCaretPos) {
- e.text.SetCaret(start, prevCaretPos)
- }
- } else {
- e.text.ClearSelection()
- }
- e.dragging = true
- // Process multi-clicks.
- switch {
- case evt.NumClicks == 2:
- e.text.MoveWord(-1, selectionClear)
- e.text.MoveWord(1, selectionExtend)
- e.dragging = false
- case evt.NumClicks >= 3:
- e.text.MoveLineStart(selectionClear)
- e.text.MoveLineEnd(selectionExtend)
- e.dragging = false
- }
- }
- case pointer.Event:
- release := false
- switch {
- case evt.Kind == pointer.Release && evt.Source == pointer.Mouse:
- release = true
- fallthrough
- case evt.Kind == pointer.Drag && evt.Source == pointer.Mouse:
- if e.dragging {
- e.blinkStart = gtx.Now
- e.text.MoveCoord(image.Point{
- X: int(math.Round(float64(evt.Position.X))),
- Y: int(math.Round(float64(evt.Position.Y))),
- })
- e.scrollCaret = true
- if release {
- e.dragging = false
- }
- }
- }
- }
- return nil, false
- }
- func condFilter(pred bool, f key.Filter) event.Filter {
- if pred {
- return f
- } else {
- return nil
- }
- }
- func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
- if e.text.Changed() {
- return ChangeEvent{}, true
- }
- caret, _ := e.text.Selection()
- atBeginning := caret == 0
- atEnd := caret == e.text.Len()
- if gtx.Locale.Direction.Progression() != system.FromOrigin {
- atEnd, atBeginning = atBeginning, atEnd
- }
- filters := []event.Filter{
- key.FocusFilter{Target: e},
- transfer.TargetFilter{Target: e, Type: "application/text"},
- key.Filter{Focus: e, Name: key.NameEnter, Optional: key.ModShift},
- key.Filter{Focus: e, Name: key.NameReturn, Optional: key.ModShift},
- key.Filter{Focus: e, Name: "Z", Required: key.ModShortcut, Optional: key.ModShift},
- key.Filter{Focus: e, Name: "C", Required: key.ModShortcut},
- key.Filter{Focus: e, Name: "V", Required: key.ModShortcut},
- key.Filter{Focus: e, Name: "X", Required: key.ModShortcut},
- key.Filter{Focus: e, Name: "A", Required: key.ModShortcut},
- key.Filter{Focus: e, Name: key.NameDeleteBackward, Optional: key.ModShortcutAlt | key.ModShift},
- key.Filter{Focus: e, Name: key.NameDeleteForward, Optional: key.ModShortcutAlt | key.ModShift},
- key.Filter{Focus: e, Name: key.NameHome, Optional: key.ModShortcut | key.ModShift},
- key.Filter{Focus: e, Name: key.NameEnd, Optional: key.ModShortcut | key.ModShift},
- key.Filter{Focus: e, Name: key.NamePageDown, Optional: key.ModShift},
- key.Filter{Focus: e, Name: key.NamePageUp, Optional: key.ModShift},
- condFilter(!atBeginning, key.Filter{Focus: e, Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}),
- condFilter(!atBeginning, key.Filter{Focus: e, Name: key.NameUpArrow, Optional: key.ModShortcutAlt | key.ModShift}),
- condFilter(!atEnd, key.Filter{Focus: e, Name: key.NameRightArrow, Optional: key.ModShortcutAlt | key.ModShift}),
- condFilter(!atEnd, key.Filter{Focus: e, Name: key.NameDownArrow, Optional: key.ModShortcutAlt | key.ModShift}),
- }
- // adjust keeps track of runes dropped because of MaxLen.
- var adjust int
- for {
- ke, ok := gtx.Event(filters...)
- if !ok {
- break
- }
- e.blinkStart = gtx.Now
- switch ke := ke.(type) {
- case key.FocusEvent:
- // Reset IME state.
- e.ime.imeState = imeState{}
- if ke.Focus && !e.ReadOnly {
- gtx.Execute(key.SoftKeyboardCmd{Show: true})
- }
- case key.Event:
- if !gtx.Focused(e) || ke.State != key.Press {
- break
- }
- if !e.ReadOnly && e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) {
- if !ke.Modifiers.Contain(key.ModShift) {
- e.scratch = e.text.Text(e.scratch)
- return SubmitEvent{
- Text: string(e.scratch),
- }, true
- }
- }
- e.scrollCaret = true
- e.scroller.Stop()
- ev, ok := e.command(gtx, ke)
- if ok {
- return ev, ok
- }
- case key.SnippetEvent:
- e.updateSnippet(gtx, ke.Start, ke.End)
- case key.EditEvent:
- if e.ReadOnly {
- break
- }
- e.scrollCaret = true
- e.scroller.Stop()
- s := ke.Text
- moves := 0
- submit := false
- switch {
- case e.Submit:
- if i := strings.IndexByte(s, '\n'); i != -1 {
- submit = true
- moves += len(s) - i
- s = s[:i]
- }
- case e.SingleLine:
- s = strings.ReplaceAll(s, "\n", " ")
- }
- moves += e.replace(ke.Range.Start, ke.Range.End, s, true)
- adjust += utf8.RuneCountInString(ke.Text) - moves
- // Reset caret xoff.
- e.text.MoveCaret(0, 0)
- if submit {
- e.scratch = e.text.Text(e.scratch)
- submitEvent := SubmitEvent{
- Text: string(e.scratch),
- }
- if e.text.Changed() {
- e.pending = append(e.pending, submitEvent)
- return ChangeEvent{}, true
- }
- return submitEvent, true
- }
- // Complete a paste event, initiated by Shortcut-V in Editor.command().
- case transfer.DataEvent:
- e.scrollCaret = true
- e.scroller.Stop()
- content, err := io.ReadAll(ke.Open())
- if err == nil {
- if e.Insert(string(content)) != 0 {
- return ChangeEvent{}, true
- }
- }
- case key.SelectionEvent:
- e.scrollCaret = true
- e.scroller.Stop()
- ke.Start -= adjust
- ke.End -= adjust
- adjust = 0
- e.text.SetCaret(ke.Start, ke.End)
- }
- }
- if e.text.Changed() {
- return ChangeEvent{}, true
- }
- return nil, false
- }
- func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) {
- direction := 1
- if gtx.Locale.Direction.Progression() == system.TowardOrigin {
- direction = -1
- }
- moveByWord := k.Modifiers.Contain(key.ModShortcutAlt)
- selAct := selectionClear
- if k.Modifiers.Contain(key.ModShift) {
- selAct = selectionExtend
- }
- if k.Modifiers.Contain(key.ModShortcut) {
- switch k.Name {
- // Initiate a paste operation, by requesting the clipboard contents; other
- // half is in Editor.processKey() under clipboard.Event.
- case "V":
- if !e.ReadOnly {
- gtx.Execute(clipboard.ReadCmd{Tag: e})
- }
- // Copy or Cut selection -- ignored if nothing selected.
- case "C", "X":
- e.scratch = e.text.SelectedText(e.scratch)
- if text := string(e.scratch); text != "" {
- gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(strings.NewReader(text))})
- if k.Name == "X" && !e.ReadOnly {
- if e.Delete(1) != 0 {
- return ChangeEvent{}, true
- }
- }
- }
- // Select all
- case "A":
- e.text.SetCaret(0, e.text.Len())
- case "Z":
- if !e.ReadOnly {
- if k.Modifiers.Contain(key.ModShift) {
- if ev, ok := e.redo(); ok {
- return ev, ok
- }
- } else {
- if ev, ok := e.undo(); ok {
- return ev, ok
- }
- }
- }
- case key.NameHome:
- e.text.MoveTextStart(selAct)
- case key.NameEnd:
- e.text.MoveTextEnd(selAct)
- }
- return nil, false
- }
- switch k.Name {
- case key.NameReturn, key.NameEnter:
- if !e.ReadOnly {
- if e.Insert("\n") != 0 {
- return ChangeEvent{}, true
- }
- }
- case key.NameDeleteBackward:
- if !e.ReadOnly {
- if moveByWord {
- if e.deleteWord(-1) != 0 {
- return ChangeEvent{}, true
- }
- } else {
- if e.Delete(-1) != 0 {
- return ChangeEvent{}, true
- }
- }
- }
- case key.NameDeleteForward:
- if !e.ReadOnly {
- if moveByWord {
- if e.deleteWord(1) != 0 {
- return ChangeEvent{}, true
- }
- } else {
- if e.Delete(1) != 0 {
- return ChangeEvent{}, true
- }
- }
- }
- case key.NameUpArrow:
- e.text.MoveLines(-1, selAct)
- case key.NameDownArrow:
- e.text.MoveLines(+1, selAct)
- case key.NameLeftArrow:
- if moveByWord {
- e.text.MoveWord(-1*direction, selAct)
- } else {
- if selAct == selectionClear {
- e.text.ClearSelection()
- }
- e.text.MoveCaret(-1*direction, -1*direction*int(selAct))
- }
- case key.NameRightArrow:
- if moveByWord {
- e.text.MoveWord(1*direction, selAct)
- } else {
- if selAct == selectionClear {
- e.text.ClearSelection()
- }
- e.text.MoveCaret(1*direction, int(selAct)*direction)
- }
- case key.NamePageUp:
- e.text.MovePages(-1, selAct)
- case key.NamePageDown:
- e.text.MovePages(+1, selAct)
- case key.NameHome:
- e.text.MoveLineStart(selAct)
- case key.NameEnd:
- e.text.MoveLineEnd(selAct)
- }
- return nil, false
- }
- // initBuffer should be invoked first in every exported function that accesses
- // text state. It ensures that the underlying text widget is both ready to use
- // and has its fields synced with the editor.
- func (e *Editor) initBuffer() {
- if e.buffer == nil {
- e.buffer = new(editBuffer)
- e.text.SetSource(e.buffer)
- }
- e.text.Alignment = e.Alignment
- e.text.LineHeight = e.LineHeight
- e.text.LineHeightScale = e.LineHeightScale
- e.text.SingleLine = e.SingleLine
- e.text.Mask = e.Mask
- e.text.WrapPolicy = e.WrapPolicy
- e.text.DisableSpaceTrim = true
- }
- // Update the state of the editor in response to input events. Update consumes editor
- // input events until there are no remaining events or an editor event is generated.
- // To fully update the state of the editor, callers should call Update until it returns
- // false.
- func (e *Editor) Update(gtx layout.Context) (EditorEvent, bool) {
- e.initBuffer()
- event, ok := e.processEvents(gtx)
- // Notify IME of selection if it changed.
- newSel := e.ime.selection
- start, end := e.text.Selection()
- newSel.rng = key.Range{
- Start: start,
- End: end,
- }
- caretPos, carAsc, carDesc := e.text.CaretInfo()
- newSel.caret = key.Caret{
- Pos: layout.FPt(caretPos),
- Ascent: float32(carAsc),
- Descent: float32(carDesc),
- }
- if newSel != e.ime.selection {
- e.ime.selection = newSel
- gtx.Execute(key.SelectionCmd{Tag: e, Range: newSel.rng, Caret: newSel.caret})
- }
- e.updateSnippet(gtx, e.ime.start, e.ime.end)
- return event, ok
- }
- // Layout lays out the editor using the provided textMaterial as the paint material
- // for the text glyphs+caret and the selectMaterial as the paint material for the
- // selection rectangle.
- func (e *Editor) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, textMaterial, selectMaterial op.CallOp) layout.Dimensions {
- for {
- _, ok := e.Update(gtx)
- if !ok {
- break
- }
- }
- e.text.Layout(gtx, lt, font, size)
- return e.layout(gtx, textMaterial, selectMaterial)
- }
- // updateSnippet queues a key.SnippetCmd if the snippet content or position
- // have changed. off and len are in runes.
- func (e *Editor) updateSnippet(gtx layout.Context, start, end int) {
- if start > end {
- start, end = end, start
- }
- length := e.text.Len()
- if start > length {
- start = length
- }
- if end > length {
- end = length
- }
- e.ime.start = start
- e.ime.end = end
- startOff := e.text.ByteOffset(start)
- endOff := e.text.ByteOffset(end)
- n := endOff - startOff
- if n > int64(len(e.ime.scratch)) {
- e.ime.scratch = make([]byte, n)
- }
- scratch := e.ime.scratch[:n]
- read, _ := e.text.ReadAt(scratch, startOff)
- if read != len(scratch) {
- panic("e.rr.Read truncated data")
- }
- newSnip := key.Snippet{
- Range: key.Range{
- Start: e.ime.start,
- End: e.ime.end,
- },
- Text: e.ime.snippet.Text,
- }
- if string(scratch) != newSnip.Text {
- newSnip.Text = string(scratch)
- }
- if newSnip == e.ime.snippet {
- return
- }
- e.ime.snippet = newSnip
- gtx.Execute(key.SnippetCmd{Tag: e, Snippet: newSnip})
- }
- func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.CallOp) layout.Dimensions {
- // Adjust scrolling for new viewport and layout.
- e.text.ScrollRel(0, 0)
- if e.scrollCaret {
- e.scrollCaret = false
- e.text.ScrollToCaret()
- }
- visibleDims := e.text.Dimensions()
- defer clip.Rect(image.Rectangle{Max: visibleDims.Size}).Push(gtx.Ops).Pop()
- pointer.CursorText.Add(gtx.Ops)
- event.Op(gtx.Ops, e)
- key.InputHintOp{Tag: e, Hint: e.InputHint}.Add(gtx.Ops)
- e.scroller.Add(gtx.Ops)
- e.clicker.Add(gtx.Ops)
- e.dragger.Add(gtx.Ops)
- e.showCaret = false
- if gtx.Focused(e) {
- now := gtx.Now
- dt := now.Sub(e.blinkStart)
- blinking := dt < maxBlinkDuration
- const timePerBlink = time.Second / blinksPerSecond
- nextBlink := now.Add(timePerBlink/2 - dt%(timePerBlink/2))
- if blinking {
- gtx.Execute(op.InvalidateCmd{At: nextBlink})
- }
- e.showCaret = !blinking || dt%timePerBlink < timePerBlink/2
- }
- semantic.Editor.Add(gtx.Ops)
- if e.Len() > 0 {
- e.paintSelection(gtx, selectMaterial)
- e.paintText(gtx, textMaterial)
- }
- if gtx.Enabled() {
- e.paintCaret(gtx, textMaterial)
- }
- return visibleDims
- }
- // paintSelection paints the contrasting background for selected text using the provided
- // material to set the painting material for the selection.
- func (e *Editor) paintSelection(gtx layout.Context, material op.CallOp) {
- e.initBuffer()
- if !gtx.Focused(e) {
- return
- }
- e.text.PaintSelection(gtx, material)
- }
- // paintText paints the text glyphs using the provided material to set the fill of the
- // glyphs.
- func (e *Editor) paintText(gtx layout.Context, material op.CallOp) {
- e.initBuffer()
- e.text.PaintText(gtx, material)
- }
- // paintCaret paints the text glyphs using the provided material to set the fill material
- // of the caret rectangle.
- func (e *Editor) paintCaret(gtx layout.Context, material op.CallOp) {
- e.initBuffer()
- if !e.showCaret || e.ReadOnly {
- return
- }
- e.text.PaintCaret(gtx, material)
- }
- // Len is the length of the editor contents, in runes.
- func (e *Editor) Len() int {
- e.initBuffer()
- return e.text.Len()
- }
- // Text returns the contents of the editor.
- func (e *Editor) Text() string {
- e.initBuffer()
- e.scratch = e.text.Text(e.scratch)
- return string(e.scratch)
- }
- func (e *Editor) SetText(s string) {
- e.initBuffer()
- if e.SingleLine {
- s = strings.ReplaceAll(s, "\n", " ")
- }
- e.replace(0, e.text.Len(), s, true)
- // Reset xoff and move the caret to the beginning.
- e.SetCaret(0, 0)
- }
- // CaretPos returns the line & column numbers of the caret.
- func (e *Editor) CaretPos() (line, col int) {
- e.initBuffer()
- return e.text.CaretPos()
- }
- // CaretCoords returns the coordinates of the caret, relative to the
- // editor itself.
- func (e *Editor) CaretCoords() f32.Point {
- e.initBuffer()
- return e.text.CaretCoords()
- }
- // Delete runes from the caret position. The sign of the argument specifies the
- // direction to delete: positive is forward, negative is backward.
- //
- // If there is a selection, it is deleted and counts as a single grapheme
- // cluster.
- func (e *Editor) Delete(graphemeClusters int) (deletedRunes int) {
- e.initBuffer()
- if graphemeClusters == 0 {
- return 0
- }
- start, end := e.text.Selection()
- if start != end {
- graphemeClusters -= sign(graphemeClusters)
- }
- // Move caret by the target quantity of clusters.
- e.text.MoveCaret(0, graphemeClusters)
- // Get the new rune offsets of the selection.
- start, end = e.text.Selection()
- e.replace(start, end, "", true)
- // Reset xoff.
- e.text.MoveCaret(0, 0)
- e.ClearSelection()
- return end - start
- }
- func (e *Editor) Insert(s string) (insertedRunes int) {
- e.initBuffer()
- if e.SingleLine {
- s = strings.ReplaceAll(s, "\n", " ")
- }
- start, end := e.text.Selection()
- moves := e.replace(start, end, s, true)
- if end < start {
- start = end
- }
- // Reset xoff.
- e.text.MoveCaret(0, 0)
- e.SetCaret(start+moves, start+moves)
- e.scrollCaret = true
- return moves
- }
- // modification represents a change to the contents of the editor buffer.
- // It contains the necessary information to both apply the change and
- // reverse it, and is useful for implementing undo/redo.
- type modification struct {
- // StartRune is the inclusive index of the first rune
- // modified.
- StartRune int
- // ApplyContent is the data inserted at StartRune to
- // apply this operation. It overwrites len([]rune(ReverseContent)) runes.
- ApplyContent string
- // ReverseContent is the data inserted at StartRune to
- // apply this operation. It overwrites len([]rune(ApplyContent)) runes.
- ReverseContent string
- }
- // undo applies the modification at e.history[e.historyIdx] and decrements
- // e.historyIdx.
- func (e *Editor) undo() (EditorEvent, bool) {
- e.initBuffer()
- if len(e.history) < 1 || e.nextHistoryIdx == 0 {
- return nil, false
- }
- mod := e.history[e.nextHistoryIdx-1]
- replaceEnd := mod.StartRune + utf8.RuneCountInString(mod.ApplyContent)
- e.replace(mod.StartRune, replaceEnd, mod.ReverseContent, false)
- caretEnd := mod.StartRune + utf8.RuneCountInString(mod.ReverseContent)
- e.SetCaret(caretEnd, mod.StartRune)
- e.nextHistoryIdx--
- return ChangeEvent{}, true
- }
- // redo applies the modification at e.history[e.historyIdx] and increments
- // e.historyIdx.
- func (e *Editor) redo() (EditorEvent, bool) {
- e.initBuffer()
- if len(e.history) < 1 || e.nextHistoryIdx == len(e.history) {
- return nil, false
- }
- mod := e.history[e.nextHistoryIdx]
- end := mod.StartRune + utf8.RuneCountInString(mod.ReverseContent)
- e.replace(mod.StartRune, end, mod.ApplyContent, false)
- caretEnd := mod.StartRune + utf8.RuneCountInString(mod.ApplyContent)
- e.SetCaret(caretEnd, mod.StartRune)
- e.nextHistoryIdx++
- return ChangeEvent{}, true
- }
- // replace the text between start and end with s. Indices are in runes.
- // It returns the number of runes inserted.
- // addHistory controls whether this modification is recorded in the undo
- // history. replace can modify text in positions unrelated to the cursor
- // position.
- func (e *Editor) replace(start, end int, s string, addHistory bool) int {
- length := e.text.Len()
- if start > end {
- start, end = end, start
- }
- start = min(start, length)
- end = min(end, length)
- replaceSize := end - start
- el := e.Len()
- var sc int
- idx := 0
- for idx < len(s) {
- if e.MaxLen > 0 && el-replaceSize+sc >= e.MaxLen {
- s = s[:idx]
- break
- }
- _, n := utf8.DecodeRuneInString(s[idx:])
- if e.Filter != "" && !strings.Contains(e.Filter, s[idx:idx+n]) {
- s = s[:idx] + s[idx+n:]
- continue
- }
- idx += n
- sc++
- }
- if addHistory {
- deleted := make([]rune, 0, replaceSize)
- readPos := e.text.ByteOffset(start)
- for i := 0; i < replaceSize; i++ {
- ru, s, _ := e.text.ReadRuneAt(int64(readPos))
- readPos += int64(s)
- deleted = append(deleted, ru)
- }
- if e.nextHistoryIdx < len(e.history) {
- e.history = e.history[:e.nextHistoryIdx]
- }
- e.history = append(e.history, modification{
- StartRune: start,
- ApplyContent: s,
- ReverseContent: string(deleted),
- })
- e.nextHistoryIdx++
- }
- sc = e.text.Replace(start, end, s)
- newEnd := start + sc
- adjust := func(pos int) int {
- switch {
- case newEnd < pos && pos <= end:
- pos = newEnd
- case end < pos:
- diff := newEnd - end
- pos = pos + diff
- }
- return pos
- }
- e.ime.start = adjust(e.ime.start)
- e.ime.end = adjust(e.ime.end)
- return sc
- }
- // MoveCaret moves the caret (aka selection start) and the selection end
- // relative to their current positions. Positive distances moves forward,
- // negative distances moves backward. Distances are in grapheme clusters,
- // which closely match what users perceive as "characters" even when the
- // characters are multiple code points long.
- func (e *Editor) MoveCaret(startDelta, endDelta int) {
- e.initBuffer()
- e.text.MoveCaret(startDelta, endDelta)
- }
- // deleteWord deletes the next word(s) in the specified direction.
- // Unlike moveWord, deleteWord treats whitespace as a word itself.
- // Positive is forward, negative is backward.
- // Absolute values greater than one will delete that many words.
- // The selection counts as a single word.
- func (e *Editor) deleteWord(distance int) (deletedRunes int) {
- if distance == 0 {
- return
- }
- start, end := e.text.Selection()
- if start != end {
- deletedRunes = e.Delete(1)
- distance -= sign(distance)
- }
- if distance == 0 {
- return deletedRunes
- }
- // split the distance information into constituent parts to be
- // used independently.
- words, direction := distance, 1
- if distance < 0 {
- words, direction = distance*-1, -1
- }
- caret, _ := e.text.Selection()
- // atEnd if offset is at or beyond either side of the buffer.
- atEnd := func(runes int) bool {
- idx := caret + runes*direction
- return idx <= 0 || idx >= e.Len()
- }
- // next returns the appropriate rune given the direction and offset in runes).
- next := func(runes int) rune {
- idx := caret + runes*direction
- if idx < 0 {
- idx = 0
- } else if idx > e.Len() {
- idx = e.Len()
- }
- off := e.text.ByteOffset(idx)
- var r rune
- if direction < 0 {
- r, _, _ = e.text.ReadRuneBefore(int64(off))
- } else {
- r, _, _ = e.text.ReadRuneAt(int64(off))
- }
- return r
- }
- runes := 1
- for ii := 0; ii < words; ii++ {
- r := next(runes)
- wantSpace := unicode.IsSpace(r)
- for r := next(runes); unicode.IsSpace(r) == wantSpace && !atEnd(runes); r = next(runes) {
- runes += 1
- }
- }
- deletedRunes += e.Delete(runes * direction)
- return deletedRunes
- }
- // SelectionLen returns the length of the selection, in runes; it is
- // equivalent to utf8.RuneCountInString(e.SelectedText()).
- func (e *Editor) SelectionLen() int {
- e.initBuffer()
- return e.text.SelectionLen()
- }
- // Selection returns the start and end of the selection, as rune offsets.
- // start can be > end.
- func (e *Editor) Selection() (start, end int) {
- e.initBuffer()
- return e.text.Selection()
- }
- // SetCaret moves the caret to start, and sets the selection end to end. start
- // and end are in runes, and represent offsets into the editor text.
- func (e *Editor) SetCaret(start, end int) {
- e.initBuffer()
- e.text.SetCaret(start, end)
- e.scrollCaret = true
- e.scroller.Stop()
- }
- // SelectedText returns the currently selected text (if any) from the editor.
- func (e *Editor) SelectedText() string {
- e.initBuffer()
- e.scratch = e.text.SelectedText(e.scratch)
- return string(e.scratch)
- }
- // ClearSelection clears the selection, by setting the selection end equal to
- // the selection start.
- func (e *Editor) ClearSelection() {
- e.initBuffer()
- e.text.ClearSelection()
- }
- // WriteTo implements io.WriterTo.
- func (e *Editor) WriteTo(w io.Writer) (int64, error) {
- e.initBuffer()
- return e.text.WriteTo(w)
- }
- // Seek implements io.Seeker.
- func (e *Editor) Seek(offset int64, whence int) (int64, error) {
- e.initBuffer()
- return e.text.Seek(offset, whence)
- }
- // Read implements io.Reader.
- func (e *Editor) Read(p []byte) (int, error) {
- e.initBuffer()
- return e.text.Read(p)
- }
- // Regions returns visible regions covering the rune range [start,end).
- func (e *Editor) Regions(start, end int, regions []Region) []Region {
- e.initBuffer()
- return e.text.Regions(start, end, regions)
- }
- func max(a, b int) int {
- if a > b {
- return a
- }
- return b
- }
- func min(a, b int) int {
- if a < b {
- return a
- }
- return b
- }
- func abs(n int) int {
- if n < 0 {
- return -n
- }
- return n
- }
- func sign(n int) int {
- switch {
- case n < 0:
- return -1
- case n > 0:
- return 1
- default:
- return 0
- }
- }
- func (s ChangeEvent) isEditorEvent() {}
- func (s SubmitEvent) isEditorEvent() {}
- func (s SelectEvent) isEditorEvent() {}
|