mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-08 19:05:35 +00:00
widget: [API] convert Editor to return one event at a time
This commit eliminates (*widget.Editor).Events() in favor of making
(*widget.Editor).Update() return events as they are generated in response to
input. This makes the behavior of the editor match the rest of the core widgets.
Callers who previously invoked Events() can now achieve the same thing by using
a loop like this:
for {
ev, ok := editor.Update(gtx)
if !ok {
break
}
// Handle ev
}
This is undeniably more verbose, but it enables more sophisticated event processing.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit is contained in:
+184
-137
@@ -87,16 +87,14 @@ type Editor struct {
|
|||||||
|
|
||||||
clicker gesture.Click
|
clicker gesture.Click
|
||||||
|
|
||||||
// events is the list of events not yet processed.
|
|
||||||
events []EditorEvent
|
|
||||||
// prevEvents is the number of events from the previous frame.
|
|
||||||
prevEvents int
|
|
||||||
// history contains undo history.
|
// history contains undo history.
|
||||||
history []modification
|
history []modification
|
||||||
// nextHistoryIdx is the index within the history of the next modification. This
|
// 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
|
// is only not len(history) immediately after undo operations occur. It is framed as the "next" value
|
||||||
// to make the zero value consistent.
|
// to make the zero value consistent.
|
||||||
nextHistoryIdx int
|
nextHistoryIdx int
|
||||||
|
|
||||||
|
pending []EditorEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
type offEntry struct {
|
type offEntry struct {
|
||||||
@@ -189,30 +187,37 @@ const (
|
|||||||
maxBlinkDuration = 10 * time.Second
|
maxBlinkDuration = 10 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// Events returns available editor events.
|
func (e *Editor) processEvents(gtx layout.Context) (ev EditorEvent, ok bool) {
|
||||||
func (e *Editor) Events() []EditorEvent {
|
if len(e.pending) > 0 {
|
||||||
events := e.events
|
out := e.pending[0]
|
||||||
e.events = nil
|
e.pending = e.pending[:copy(e.pending, e.pending[1:])]
|
||||||
e.prevEvents = 0
|
return out, true
|
||||||
return events
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Editor) processEvents(gtx layout.Context) {
|
|
||||||
// Flush events from before the previous Layout.
|
|
||||||
n := copy(e.events, e.events[e.prevEvents:])
|
|
||||||
e.events = e.events[:n]
|
|
||||||
e.prevEvents = n
|
|
||||||
|
|
||||||
oldStart, oldLen := min(e.text.Selection()), e.text.SelectionLen()
|
|
||||||
e.processPointer(gtx)
|
|
||||||
e.processKey(gtx)
|
|
||||||
// Queue a SelectEvent if the selection changed, including if it went away.
|
|
||||||
if newStart, newLen := min(e.text.Selection()), e.text.SelectionLen(); oldStart != newStart || oldLen != newLen {
|
|
||||||
e.events = append(e.events, SelectEvent{})
|
|
||||||
}
|
}
|
||||||
|
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) {
|
func (e *Editor) processPointer(gtx layout.Context) (EditorEvent, bool) {
|
||||||
sbounds := e.text.ScrollBounds()
|
sbounds := e.text.ScrollBounds()
|
||||||
var smin, smax int
|
var smin, smax int
|
||||||
var axis gesture.Axis
|
var axis gesture.Axis
|
||||||
@@ -244,92 +249,96 @@ func (e *Editor) processPointer(gtx layout.Context) {
|
|||||||
e.text.ScrollRel(0, sdist)
|
e.text.ScrollRel(0, sdist)
|
||||||
soff = e.text.ScrollOff().Y
|
soff = e.text.ScrollOff().Y
|
||||||
}
|
}
|
||||||
for _, evt := range e.clickDragEvents(gtx) {
|
|
||||||
switch evt := evt.(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.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.MoveStart(selectionClear)
|
|
||||||
e.text.MoveEnd(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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sdist > 0 && soff >= smax) || (sdist < 0 && soff <= smin) {
|
|
||||||
e.scroller.Stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Editor) clickDragEvents(gtx layout.Context) []event.Event {
|
|
||||||
var combinedEvents []event.Event
|
|
||||||
for {
|
for {
|
||||||
evt, ok := e.clicker.Update(gtx.Source)
|
evt, ok := e.clicker.Update(gtx.Source)
|
||||||
if !ok {
|
if !ok {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
combinedEvents = append(combinedEvents, evt)
|
ev, ok := e.processPointerEvent(gtx, evt)
|
||||||
|
if ok {
|
||||||
|
return ev, ok
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
evt, ok := e.dragger.Update(gtx.Metric, gtx.Source, gesture.Both)
|
evt, ok := e.dragger.Update(gtx.Metric, gtx.Source, gesture.Both)
|
||||||
if !ok {
|
if !ok {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
combinedEvents = append(combinedEvents, evt)
|
ev, ok := e.processPointerEvent(gtx, evt)
|
||||||
|
if ok {
|
||||||
|
return ev, ok
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return combinedEvents
|
|
||||||
|
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.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.MoveStart(selectionClear)
|
||||||
|
e.text.MoveEnd(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 {
|
func condFilter(pred bool, f key.Filter) event.Filter {
|
||||||
@@ -340,9 +349,9 @@ func condFilter(pred bool, f key.Filter) event.Filter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Editor) processKey(gtx layout.Context) {
|
func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
|
||||||
if e.text.Changed() {
|
if e.text.Changed() {
|
||||||
e.events = append(e.events, ChangeEvent{})
|
return ChangeEvent{}, true
|
||||||
}
|
}
|
||||||
caret, _ := e.text.Selection()
|
caret, _ := e.text.Selection()
|
||||||
atBeginning := caret == 0
|
atBeginning := caret == 0
|
||||||
@@ -396,15 +405,17 @@ func (e *Editor) processKey(gtx layout.Context) {
|
|||||||
if !e.ReadOnly && e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) {
|
if !e.ReadOnly && e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) {
|
||||||
if !ke.Modifiers.Contain(key.ModShift) {
|
if !ke.Modifiers.Contain(key.ModShift) {
|
||||||
e.scratch = e.text.Text(e.scratch)
|
e.scratch = e.text.Text(e.scratch)
|
||||||
e.events = append(e.events, SubmitEvent{
|
return SubmitEvent{
|
||||||
Text: string(e.scratch),
|
Text: string(e.scratch),
|
||||||
})
|
}, true
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
e.command(gtx, ke)
|
|
||||||
e.scrollCaret = true
|
e.scrollCaret = true
|
||||||
e.scroller.Stop()
|
e.scroller.Stop()
|
||||||
|
ev, ok := e.command(gtx, ke)
|
||||||
|
if ok {
|
||||||
|
return ev, ok
|
||||||
|
}
|
||||||
case key.SnippetEvent:
|
case key.SnippetEvent:
|
||||||
e.updateSnippet(gtx, ke.Start, ke.End)
|
e.updateSnippet(gtx, ke.Start, ke.End)
|
||||||
case key.EditEvent:
|
case key.EditEvent:
|
||||||
@@ -431,13 +442,15 @@ func (e *Editor) processKey(gtx layout.Context) {
|
|||||||
// Reset caret xoff.
|
// Reset caret xoff.
|
||||||
e.text.MoveCaret(0, 0)
|
e.text.MoveCaret(0, 0)
|
||||||
if submit {
|
if submit {
|
||||||
if e.text.Changed() {
|
|
||||||
e.events = append(e.events, ChangeEvent{})
|
|
||||||
}
|
|
||||||
e.scratch = e.text.Text(e.scratch)
|
e.scratch = e.text.Text(e.scratch)
|
||||||
e.events = append(e.events, SubmitEvent{
|
submitEvent := SubmitEvent{
|
||||||
Text: string(e.scratch),
|
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().
|
// Complete a paste event, initiated by Shortcut-V in Editor.command().
|
||||||
case transfer.DataEvent:
|
case transfer.DataEvent:
|
||||||
@@ -445,7 +458,9 @@ func (e *Editor) processKey(gtx layout.Context) {
|
|||||||
e.scroller.Stop()
|
e.scroller.Stop()
|
||||||
content, err := io.ReadAll(ke.Open())
|
content, err := io.ReadAll(ke.Open())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
e.Insert(string(content))
|
if e.Insert(string(content)) != 0 {
|
||||||
|
return ChangeEvent{}, true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case key.SelectionEvent:
|
case key.SelectionEvent:
|
||||||
e.scrollCaret = true
|
e.scrollCaret = true
|
||||||
@@ -457,11 +472,12 @@ func (e *Editor) processKey(gtx layout.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if e.text.Changed() {
|
if e.text.Changed() {
|
||||||
e.events = append(e.events, ChangeEvent{})
|
return ChangeEvent{}, true
|
||||||
}
|
}
|
||||||
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Editor) command(gtx layout.Context, k key.Event) {
|
func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) {
|
||||||
direction := 1
|
direction := 1
|
||||||
if gtx.Locale.Direction.Progression() == system.TowardOrigin {
|
if gtx.Locale.Direction.Progression() == system.TowardOrigin {
|
||||||
direction = -1
|
direction = -1
|
||||||
@@ -485,7 +501,9 @@ func (e *Editor) command(gtx layout.Context, k key.Event) {
|
|||||||
if text := string(e.scratch); text != "" {
|
if text := string(e.scratch); text != "" {
|
||||||
gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(strings.NewReader(text))})
|
gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(strings.NewReader(text))})
|
||||||
if k.Name == "X" && !e.ReadOnly {
|
if k.Name == "X" && !e.ReadOnly {
|
||||||
e.Delete(1)
|
if e.Delete(1) != 0 {
|
||||||
|
return ChangeEvent{}, true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Select all
|
// Select all
|
||||||
@@ -494,33 +512,47 @@ func (e *Editor) command(gtx layout.Context, k key.Event) {
|
|||||||
case "Z":
|
case "Z":
|
||||||
if !e.ReadOnly {
|
if !e.ReadOnly {
|
||||||
if k.Modifiers.Contain(key.ModShift) {
|
if k.Modifiers.Contain(key.ModShift) {
|
||||||
e.redo()
|
if ev, ok := e.redo(); ok {
|
||||||
|
return ev, ok
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
e.undo()
|
if ev, ok := e.undo(); ok {
|
||||||
|
return ev, ok
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return nil, false
|
||||||
}
|
}
|
||||||
switch k.Name {
|
switch k.Name {
|
||||||
case key.NameReturn, key.NameEnter:
|
case key.NameReturn, key.NameEnter:
|
||||||
if !e.ReadOnly {
|
if !e.ReadOnly {
|
||||||
e.Insert("\n")
|
if e.Insert("\n") != 0 {
|
||||||
|
return ChangeEvent{}, true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case key.NameDeleteBackward:
|
case key.NameDeleteBackward:
|
||||||
if !e.ReadOnly {
|
if !e.ReadOnly {
|
||||||
if moveByWord {
|
if moveByWord {
|
||||||
e.deleteWord(-1)
|
if e.deleteWord(-1) != 0 {
|
||||||
|
return ChangeEvent{}, true
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
e.Delete(-1)
|
if e.Delete(-1) != 0 {
|
||||||
|
return ChangeEvent{}, true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case key.NameDeleteForward:
|
case key.NameDeleteForward:
|
||||||
if !e.ReadOnly {
|
if !e.ReadOnly {
|
||||||
if moveByWord {
|
if moveByWord {
|
||||||
e.deleteWord(1)
|
if e.deleteWord(1) != 0 {
|
||||||
|
return ChangeEvent{}, true
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
e.Delete(1)
|
if e.Delete(1) != 0 {
|
||||||
|
return ChangeEvent{}, true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case key.NameUpArrow:
|
case key.NameUpArrow:
|
||||||
@@ -554,6 +586,7 @@ func (e *Editor) command(gtx layout.Context, k key.Event) {
|
|||||||
case key.NameEnd:
|
case key.NameEnd:
|
||||||
e.text.MoveEnd(selAct)
|
e.text.MoveEnd(selAct)
|
||||||
}
|
}
|
||||||
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// initBuffer should be invoked first in every exported function that accesses
|
// initBuffer should be invoked first in every exported function that accesses
|
||||||
@@ -572,10 +605,13 @@ func (e *Editor) initBuffer() {
|
|||||||
e.text.WrapPolicy = e.WrapPolicy
|
e.text.WrapPolicy = e.WrapPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the state of the editor in response to input events.
|
// Update the state of the editor in response to input events. Update consumes editor
|
||||||
func (e *Editor) Update(gtx layout.Context) {
|
// 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()
|
e.initBuffer()
|
||||||
e.processEvents(gtx)
|
event, ok := e.processEvents(gtx)
|
||||||
// Notify IME of selection if it changed.
|
// Notify IME of selection if it changed.
|
||||||
newSel := e.ime.selection
|
newSel := e.ime.selection
|
||||||
start, end := e.text.Selection()
|
start, end := e.text.Selection()
|
||||||
@@ -595,13 +631,19 @@ func (e *Editor) Update(gtx layout.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
e.updateSnippet(gtx, e.ime.start, e.ime.end)
|
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
|
// 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
|
// for the text glyphs+caret and the selectMaterial as the paint material for the
|
||||||
// selection rectangle.
|
// selection rectangle.
|
||||||
func (e *Editor) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, textMaterial, selectMaterial op.CallOp) layout.Dimensions {
|
func (e *Editor) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, textMaterial, selectMaterial op.CallOp) layout.Dimensions {
|
||||||
e.Update(gtx)
|
for {
|
||||||
|
_, ok := e.Update(gtx)
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
e.text.Layout(gtx, lt, font, size)
|
e.text.Layout(gtx, lt, font, size)
|
||||||
return e.layout(gtx, textMaterial, selectMaterial)
|
return e.layout(gtx, textMaterial, selectMaterial)
|
||||||
@@ -760,10 +802,10 @@ func (e *Editor) CaretCoords() f32.Point {
|
|||||||
//
|
//
|
||||||
// If there is a selection, it is deleted and counts as a single grapheme
|
// If there is a selection, it is deleted and counts as a single grapheme
|
||||||
// cluster.
|
// cluster.
|
||||||
func (e *Editor) Delete(graphemeClusters int) {
|
func (e *Editor) Delete(graphemeClusters int) (deletedRunes int) {
|
||||||
e.initBuffer()
|
e.initBuffer()
|
||||||
if graphemeClusters == 0 {
|
if graphemeClusters == 0 {
|
||||||
return
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
start, end := e.text.Selection()
|
start, end := e.text.Selection()
|
||||||
@@ -779,9 +821,10 @@ func (e *Editor) Delete(graphemeClusters int) {
|
|||||||
// Reset xoff.
|
// Reset xoff.
|
||||||
e.text.MoveCaret(0, 0)
|
e.text.MoveCaret(0, 0)
|
||||||
e.ClearSelection()
|
e.ClearSelection()
|
||||||
|
return end - start
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Editor) Insert(s string) {
|
func (e *Editor) Insert(s string) (insertedRunes int) {
|
||||||
e.initBuffer()
|
e.initBuffer()
|
||||||
if e.SingleLine {
|
if e.SingleLine {
|
||||||
s = strings.ReplaceAll(s, "\n", " ")
|
s = strings.ReplaceAll(s, "\n", " ")
|
||||||
@@ -795,6 +838,7 @@ func (e *Editor) Insert(s string) {
|
|||||||
e.text.MoveCaret(0, 0)
|
e.text.MoveCaret(0, 0)
|
||||||
e.SetCaret(start+moves, start+moves)
|
e.SetCaret(start+moves, start+moves)
|
||||||
e.scrollCaret = true
|
e.scrollCaret = true
|
||||||
|
return moves
|
||||||
}
|
}
|
||||||
|
|
||||||
// modification represents a change to the contents of the editor buffer.
|
// modification represents a change to the contents of the editor buffer.
|
||||||
@@ -814,10 +858,10 @@ type modification struct {
|
|||||||
|
|
||||||
// undo applies the modification at e.history[e.historyIdx] and decrements
|
// undo applies the modification at e.history[e.historyIdx] and decrements
|
||||||
// e.historyIdx.
|
// e.historyIdx.
|
||||||
func (e *Editor) undo() {
|
func (e *Editor) undo() (EditorEvent, bool) {
|
||||||
e.initBuffer()
|
e.initBuffer()
|
||||||
if len(e.history) < 1 || e.nextHistoryIdx == 0 {
|
if len(e.history) < 1 || e.nextHistoryIdx == 0 {
|
||||||
return
|
return nil, false
|
||||||
}
|
}
|
||||||
mod := e.history[e.nextHistoryIdx-1]
|
mod := e.history[e.nextHistoryIdx-1]
|
||||||
replaceEnd := mod.StartRune + utf8.RuneCountInString(mod.ApplyContent)
|
replaceEnd := mod.StartRune + utf8.RuneCountInString(mod.ApplyContent)
|
||||||
@@ -825,14 +869,15 @@ func (e *Editor) undo() {
|
|||||||
caretEnd := mod.StartRune + utf8.RuneCountInString(mod.ReverseContent)
|
caretEnd := mod.StartRune + utf8.RuneCountInString(mod.ReverseContent)
|
||||||
e.SetCaret(caretEnd, mod.StartRune)
|
e.SetCaret(caretEnd, mod.StartRune)
|
||||||
e.nextHistoryIdx--
|
e.nextHistoryIdx--
|
||||||
|
return ChangeEvent{}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// redo applies the modification at e.history[e.historyIdx] and increments
|
// redo applies the modification at e.history[e.historyIdx] and increments
|
||||||
// e.historyIdx.
|
// e.historyIdx.
|
||||||
func (e *Editor) redo() {
|
func (e *Editor) redo() (EditorEvent, bool) {
|
||||||
e.initBuffer()
|
e.initBuffer()
|
||||||
if len(e.history) < 1 || e.nextHistoryIdx == len(e.history) {
|
if len(e.history) < 1 || e.nextHistoryIdx == len(e.history) {
|
||||||
return
|
return nil, false
|
||||||
}
|
}
|
||||||
mod := e.history[e.nextHistoryIdx]
|
mod := e.history[e.nextHistoryIdx]
|
||||||
end := mod.StartRune + utf8.RuneCountInString(mod.ReverseContent)
|
end := mod.StartRune + utf8.RuneCountInString(mod.ReverseContent)
|
||||||
@@ -840,6 +885,7 @@ func (e *Editor) redo() {
|
|||||||
caretEnd := mod.StartRune + utf8.RuneCountInString(mod.ApplyContent)
|
caretEnd := mod.StartRune + utf8.RuneCountInString(mod.ApplyContent)
|
||||||
e.SetCaret(caretEnd, mod.StartRune)
|
e.SetCaret(caretEnd, mod.StartRune)
|
||||||
e.nextHistoryIdx++
|
e.nextHistoryIdx++
|
||||||
|
return ChangeEvent{}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// replace the text between start and end with s. Indices are in runes.
|
// replace the text between start and end with s. Indices are in runes.
|
||||||
@@ -923,18 +969,18 @@ func (e *Editor) MoveCaret(startDelta, endDelta int) {
|
|||||||
// Positive is forward, negative is backward.
|
// Positive is forward, negative is backward.
|
||||||
// Absolute values greater than one will delete that many words.
|
// Absolute values greater than one will delete that many words.
|
||||||
// The selection counts as a single word.
|
// The selection counts as a single word.
|
||||||
func (e *Editor) deleteWord(distance int) {
|
func (e *Editor) deleteWord(distance int) (deletedRunes int) {
|
||||||
if distance == 0 {
|
if distance == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
start, end := e.text.Selection()
|
start, end := e.text.Selection()
|
||||||
if start != end {
|
if start != end {
|
||||||
e.Delete(1)
|
deletedRunes = e.Delete(1)
|
||||||
distance -= sign(distance)
|
distance -= sign(distance)
|
||||||
}
|
}
|
||||||
if distance == 0 {
|
if distance == 0 {
|
||||||
return
|
return deletedRunes
|
||||||
}
|
}
|
||||||
|
|
||||||
// split the distance information into constituent parts to be
|
// split the distance information into constituent parts to be
|
||||||
@@ -974,7 +1020,8 @@ func (e *Editor) deleteWord(distance int) {
|
|||||||
runes += 1
|
runes += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
e.Delete(runes * direction)
|
deletedRunes += e.Delete(runes * direction)
|
||||||
|
return deletedRunes
|
||||||
}
|
}
|
||||||
|
|
||||||
// SelectionLen returns the length of the selection, in runes; it is
|
// SelectionLen returns the length of the selection, in runes; it is
|
||||||
|
|||||||
+13
-9
@@ -905,7 +905,6 @@ g 2 4 6 8 g
|
|||||||
gtx.Execute(key.FocusCmd{Tag: e})
|
gtx.Execute(key.FocusCmd{Tag: e})
|
||||||
// Layout once with no events; populate e.lines.
|
// Layout once with no events; populate e.lines.
|
||||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||||
e.Events() // throw away any events from this layout
|
|
||||||
|
|
||||||
r.Frame(gtx.Ops)
|
r.Frame(gtx.Ops)
|
||||||
gtx.Source = r.Source()
|
gtx.Source = r.Source()
|
||||||
@@ -929,14 +928,13 @@ g 2 4 6 8 g
|
|||||||
)
|
)
|
||||||
tim += time.Second // Avoid multi-clicks.
|
tim += time.Second // Avoid multi-clicks.
|
||||||
|
|
||||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
for {
|
||||||
for _, evt := range e.Events() {
|
_, ok := e.Update(gtx) // throw away any events from this layout
|
||||||
switch evt.(type) {
|
if !ok {
|
||||||
case SelectEvent:
|
break
|
||||||
return e.SelectedText()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
return e.SelectedText()
|
||||||
}
|
}
|
||||||
type screenPos image.Point
|
type screenPos image.Point
|
||||||
logicalPosMatch := func(t *testing.T, n int, label string, expected screenPos, actual combinedPos) {
|
logicalPosMatch := func(t *testing.T, n int, label string, expected screenPos, actual combinedPos) {
|
||||||
@@ -1162,12 +1160,18 @@ func TestEditor_Submit(t *testing.T) {
|
|||||||
r.Queue(
|
r.Queue(
|
||||||
key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "ab1\n"},
|
key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "ab1\n"},
|
||||||
)
|
)
|
||||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
|
||||||
|
|
||||||
|
got := []EditorEvent{}
|
||||||
|
for {
|
||||||
|
ev, ok := e.Update(gtx)
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
got = append(got, ev)
|
||||||
|
}
|
||||||
if got, want := e.Text(), "ab1"; got != want {
|
if got, want := e.Text(), "ab1"; got != want {
|
||||||
t.Errorf("editor failed to filter newline")
|
t.Errorf("editor failed to filter newline")
|
||||||
}
|
}
|
||||||
got := e.Events()
|
|
||||||
want := []EditorEvent{
|
want := []EditorEvent{
|
||||||
ChangeEvent{},
|
ChangeEvent{},
|
||||||
SubmitEvent{Text: e.Text()},
|
SubmitEvent{Text: e.Text()},
|
||||||
|
|||||||
Reference in New Issue
Block a user