io/input,io/clipboard: [API] replace ReadOp with command

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2023-10-19 17:27:03 -05:00
parent d51aea553f
commit 676b670119
13 changed files with 107 additions and 67 deletions
+9 -2
View File
@@ -123,12 +123,14 @@ import (
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
"io"
"math" "math"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"runtime/cgo" "runtime/cgo"
"runtime/debug" "runtime/debug"
"strings"
"sync" "sync"
"time" "time"
"unicode/utf16" "unicode/utf16"
@@ -137,12 +139,12 @@ import (
"gioui.org/internal/f32color" "gioui.org/internal/f32color"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/io/input" "gioui.org/io/input"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/semantic" "gioui.org/io/semantic"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/unit" "gioui.org/unit"
) )
@@ -1311,7 +1313,12 @@ func (w *window) ReadClipboard() {
return return
} }
content := goString(env, C.jstring(c)) content := goString(env, C.jstring(c))
w.callbacks.Event(clipboard.Event{Text: content}) w.callbacks.Event(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
}) })
} }
+9 -2
View File
@@ -72,17 +72,19 @@ import "C"
import ( import (
"image" "image"
"io"
"runtime" "runtime"
"runtime/debug" "runtime/debug"
"strings"
"time" "time"
"unicode/utf16" "unicode/utf16"
"unsafe" "unsafe"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/unit" "gioui.org/unit"
) )
@@ -265,7 +267,12 @@ func (w *window) ReadClipboard() {
cstr := C.readClipboard() cstr := C.readClipboard()
defer C.CFRelease(cstr) defer C.CFRelease(cstr)
content := nsstringToString(cstr) content := nsstringToString(cstr)
w.w.Event(clipboard.Event{Text: content}) w.w.Event(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
} }
func (w *window) WriteClipboard(mime string, s []byte) { func (w *window) WriteClipboard(mime string, s []byte) {
+8 -2
View File
@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
"io"
"strings" "strings"
"syscall/js" "syscall/js"
"time" "time"
@@ -15,10 +16,10 @@ import (
"gioui.org/internal/f32color" "gioui.org/internal/f32color"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/unit" "gioui.org/unit"
) )
@@ -101,7 +102,12 @@ func newWindow(win *callbacks, options []Option) error {
}) })
w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} { w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} {
content := args[0].String() content := args[0].String()
go win.Event(clipboard.Event{Text: content}) go win.Event(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
return nil return nil
}) })
w.addEventListeners() w.addEventListeners()
+9 -2
View File
@@ -8,16 +8,18 @@ package app
import ( import (
"errors" "errors"
"image" "image"
"io"
"runtime" "runtime"
"strings"
"time" "time"
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
"gioui.org/internal/f32" "gioui.org/internal/f32"
"gioui.org/io/clipboard"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/unit" "gioui.org/unit"
_ "gioui.org/internal/cocoainit" _ "gioui.org/internal/cocoainit"
@@ -305,7 +307,12 @@ func (w *window) ReadClipboard() {
defer C.CFRelease(cstr) defer C.CFRelease(cstr)
} }
content := nsstringToString(cstr) content := nsstringToString(cstr)
w.w.Event(clipboard.Event{Text: content}) w.w.Event(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
} }
func (w *window) WriteClipboard(mime string, s []byte) { func (w *window) WriteClipboard(mime string, s []byte) {
+9 -5
View File
@@ -25,10 +25,10 @@ import (
"gioui.org/app/internal/xkb" "gioui.org/app/internal/xkb"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/internal/fling" "gioui.org/internal/fling"
"gioui.org/io/clipboard"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/unit" "gioui.org/unit"
) )
@@ -209,7 +209,7 @@ type window struct {
wsize image.Point // window config size before going fullscreen or maximized wsize image.Point // window config size before going fullscreen or maximized
inCompositor bool // window is moving or being resized inCompositor bool // window is moving or being resized
clipReads chan clipboard.Event clipReads chan transfer.DataEvent
wakeups chan struct{} wakeups chan struct{}
} }
@@ -354,7 +354,7 @@ func (d *wlDisplay) createNativeWindow(options []Option) (*window, error) {
ppdp: ppdp, ppdp: ppdp,
ppsp: ppdp, ppsp: ppdp,
wakeups: make(chan struct{}, 1), wakeups: make(chan struct{}, 1),
clipReads: make(chan clipboard.Event, 1), clipReads: make(chan transfer.DataEvent, 1),
} }
w.surf = C.wl_compositor_create_surface(d.compositor) w.surf = C.wl_compositor_create_surface(d.compositor)
if w.surf == nil { if w.surf == nil {
@@ -1021,14 +1021,18 @@ func (w *window) ReadClipboard() {
r, err := w.disp.readClipboard() r, err := w.disp.readClipboard()
// Send empty responses on unavailable clipboards or errors. // Send empty responses on unavailable clipboards or errors.
if r == nil || err != nil { if r == nil || err != nil {
w.w.Event(clipboard.Event{})
return return
} }
// Don't let slow clipboard transfers block event loop. // Don't let slow clipboard transfers block event loop.
go func() { go func() {
defer r.Close() defer r.Close()
data, _ := io.ReadAll(r) data, _ := io.ReadAll(r)
w.clipReads <- clipboard.Event{Text: string(data)} w.clipReads <- transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(bytes.NewReader(data))
},
}
w.Wakeup() w.Wakeup()
}() }()
} }
+8 -2
View File
@@ -6,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"image" "image"
"io"
"runtime" "runtime"
"sort" "sort"
"strings" "strings"
@@ -22,10 +23,10 @@ import (
gowindows "golang.org/x/sys/windows" gowindows "golang.org/x/sys/windows"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/io/transfer"
) )
type ViewEvent struct { type ViewEvent struct {
@@ -667,7 +668,12 @@ func (w *window) readClipboard() error {
} }
defer windows.GlobalUnlock(mem) defer windows.GlobalUnlock(mem)
content := gowindows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr))) content := gowindows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr)))
w.w.Event(clipboard.Event{Text: content}) w.w.Event(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
return nil return nil
} }
+9 -2
View File
@@ -30,16 +30,18 @@ import (
"errors" "errors"
"fmt" "fmt"
"image" "image"
"io"
"strconv" "strconv"
"strings"
"sync" "sync"
"time" "time"
"unsafe" "unsafe"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/unit" "gioui.org/unit"
syscall "golang.org/x/sys/unix" syscall "golang.org/x/sys/unix"
@@ -650,7 +652,12 @@ func (h *x11EventHandler) handleEvents() bool {
break break
} }
str := C.GoStringN((*C.char)(unsafe.Pointer(text.value)), C.int(text.nitems)) str := C.GoStringN((*C.char)(unsafe.Pointer(text.value)), C.int(text.nitems))
w.w.Event(clipboard.Event{Text: str}) w.w.Event(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(str))
},
})
case C.SelectionRequest: case C.SelectionRequest:
cevt := (*C.XSelectionRequestEvent)(unsafe.Pointer(xev)) cevt := (*C.XSelectionRequestEvent)(unsafe.Pointer(xev))
if (cevt.selection != w.atoms.clipboard && cevt.selection != w.atoms.primary) || cevt.property == C.None { if (cevt.selection != w.atoms.clipboard && cevt.selection != w.atoms.primary) || cevt.property == C.None {
-5
View File
@@ -63,7 +63,6 @@ const (
TypePass TypePass
TypePopPass TypePopPass
TypePointerInput TypePointerInput
TypeClipboardRead
TypeSource TypeSource
TypeTarget TypeTarget
TypeKeyInput TypeKeyInput
@@ -142,7 +141,6 @@ const (
TypePassLen = 1 TypePassLen = 1
TypePopPassLen = 1 TypePopPassLen = 1
TypePointerInputLen = 1 + 1 + 1*2 + 2*4 + 2*4 TypePointerInputLen = 1 + 1 + 1*2 + 2*4 + 2*4
TypeClipboardReadLen = 1
TypeSourceLen = 1 TypeSourceLen = 1
TypeTargetLen = 1 TypeTargetLen = 1
TypeKeyInputLen = 1 + 1 TypeKeyInputLen = 1 + 1
@@ -419,7 +417,6 @@ var opProps = [0x100]opProp{
TypePass: {Size: TypePassLen, NumRefs: 0}, TypePass: {Size: TypePassLen, NumRefs: 0},
TypePopPass: {Size: TypePopPassLen, NumRefs: 0}, TypePopPass: {Size: TypePopPassLen, NumRefs: 0},
TypePointerInput: {Size: TypePointerInputLen, NumRefs: 1}, TypePointerInput: {Size: TypePointerInputLen, NumRefs: 1},
TypeClipboardRead: {Size: TypeClipboardReadLen, NumRefs: 1},
TypeSource: {Size: TypeSourceLen, NumRefs: 2}, TypeSource: {Size: TypeSourceLen, NumRefs: 2},
TypeTarget: {Size: TypeTargetLen, NumRefs: 2}, TypeTarget: {Size: TypeTargetLen, NumRefs: 2},
TypeKeyInput: {Size: TypeKeyInputLen, NumRefs: 2}, TypeKeyInput: {Size: TypeKeyInputLen, NumRefs: 2},
@@ -484,8 +481,6 @@ func (t OpType) String() string {
return "PopPass" return "PopPass"
case TypePointerInput: case TypePointerInput:
return "PointerInput" return "PointerInput"
case TypeClipboardRead:
return "ClipboardRead"
case TypeSource: case TypeSource:
return "Source" return "Source"
case TypeTarget: case TypeTarget:
+4 -17
View File
@@ -5,33 +5,20 @@ package clipboard
import ( import (
"io" "io"
"gioui.org/internal/ops"
"gioui.org/io/event" "gioui.org/io/event"
"gioui.org/op"
) )
// Event is generated when the clipboard content is requested.
type Event struct {
Text string
}
// WriteCmd copies Text to the clipboard. // WriteCmd copies Text to the clipboard.
type WriteCmd struct { type WriteCmd struct {
Type string Type string
Data io.ReadCloser Data io.ReadCloser
} }
// ReadOp requests the text of the clipboard, delivered to // ReadCmd requests the text of the clipboard, delivered to
// the current handler through an Event. // the handler through an [io/transfer.DataEvent].
type ReadOp struct { type ReadCmd struct {
Tag event.Tag Tag event.Tag
} }
func (h ReadOp) Add(o *op.Ops) {
data := ops.Write1(&o.Internal, ops.TypeClipboardReadLen, h.Tag)
data[0] = byte(ops.TypeClipboardRead)
}
func (Event) ImplementsEvent() {}
func (WriteCmd) ImplementsCommand() {} func (WriteCmd) ImplementsCommand() {}
func (ReadCmd) ImplementsCommand() {}
+1 -2
View File
@@ -55,11 +55,10 @@ func (q *clipboardQueue) ProcessWriteClipboard(req clipboard.WriteCmd) {
q.text = content q.text = content
} }
func (q *clipboardQueue) ProcessReadClipboard(refs []interface{}) { func (q *clipboardQueue) ProcessReadClipboard(tag event.Tag) {
if q.receivers == nil { if q.receivers == nil {
q.receivers = make(map[event.Tag]struct{}) q.receivers = make(map[event.Tag]struct{})
} }
tag := refs[0].(event.Tag)
if _, ok := q.receivers[tag]; !ok { if _, ok := q.receivers[tag]; !ok {
q.receivers[tag] = struct{}{} q.receivers[tag] = struct{}{}
q.requested = false q.requested = false
+31 -20
View File
@@ -9,6 +9,7 @@ import (
"gioui.org/io/clipboard" "gioui.org/io/clipboard"
"gioui.org/io/event" "gioui.org/io/event"
"gioui.org/io/transfer"
"gioui.org/op" "gioui.org/op"
) )
@@ -16,30 +17,35 @@ func TestClipboardDuplicateEvent(t *testing.T) {
ops, router, handler := new(op.Ops), new(Router), make([]int, 2) ops, router, handler := new(op.Ops), new(Router), make([]int, 2)
// Both must receive the event once // Both must receive the event once
clipboard.ReadOp{Tag: &handler[0]}.Add(ops) router.Source().Queue(clipboard.ReadCmd{Tag: &handler[0]})
clipboard.ReadOp{Tag: &handler[1]}.Add(ops) router.Source().Queue(clipboard.ReadCmd{Tag: &handler[1]})
router.Frame(ops) router.Frame(ops)
event := clipboard.Event{Text: "Test"} event := transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader("Test"))
},
}
router.Queue(event) router.Queue(event)
assertClipboardReadOp(t, router, 0) assertClipboardReadCmd(t, router, 0)
assertClipboardEvent(t, router.Events(&handler[0]), true) assertClipboardEvent(t, router.Events(&handler[0]), true)
assertClipboardEvent(t, router.Events(&handler[1]), true) assertClipboardEvent(t, router.Events(&handler[1]), true)
ops.Reset() ops.Reset()
// No ReadOp // No ReadCmd
router.Frame(ops) router.Frame(ops)
assertClipboardReadOp(t, router, 0) assertClipboardReadCmd(t, router, 0)
assertClipboardEvent(t, router.Events(&handler[0]), false) assertClipboardEvent(t, router.Events(&handler[0]), false)
assertClipboardEvent(t, router.Events(&handler[1]), false) assertClipboardEvent(t, router.Events(&handler[1]), false)
ops.Reset() ops.Reset()
clipboard.ReadOp{Tag: &handler[0]}.Add(ops) router.Source().Queue(clipboard.ReadCmd{Tag: &handler[0]})
router.Frame(ops) router.Frame(ops)
// No ClipboardEvent sent // No ClipboardEvent sent
assertClipboardReadOp(t, router, 1) assertClipboardReadCmd(t, router, 1)
assertClipboardEvent(t, router.Events(&handler[0]), false) assertClipboardEvent(t, router.Events(&handler[0]), false)
assertClipboardEvent(t, router.Events(&handler[1]), false) assertClipboardEvent(t, router.Events(&handler[1]), false)
ops.Reset() ops.Reset()
@@ -50,34 +56,39 @@ func TestQueueProcessReadClipboard(t *testing.T) {
ops.Reset() ops.Reset()
// Request read // Request read
clipboard.ReadOp{Tag: &handler[0]}.Add(ops) router.Source().Queue(clipboard.ReadCmd{Tag: &handler[0]})
router.Frame(ops) router.Frame(ops)
assertClipboardReadOp(t, router, 1) assertClipboardReadCmd(t, router, 1)
ops.Reset() ops.Reset()
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
// No ReadOp // No ReadCmd
// One receiver must still wait for response // One receiver must still wait for response
router.Frame(ops) router.Frame(ops)
assertClipboardReadOpDuplicated(t, router, 1) assertClipboardReadDuplicated(t, router, 1)
ops.Reset() ops.Reset()
} }
router.Frame(ops) router.Frame(ops)
// Send the clipboard event // Send the clipboard event
event := clipboard.Event{Text: "Text 2"} event := transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader("Text 2"))
},
}
router.Queue(event) router.Queue(event)
assertClipboardReadOp(t, router, 0) assertClipboardReadCmd(t, router, 0)
assertClipboardEvent(t, router.Events(&handler[0]), true) assertClipboardEvent(t, router.Events(&handler[0]), true)
ops.Reset() ops.Reset()
// No ReadOp // No ReadCmd
// There's no receiver waiting // There's no receiver waiting
router.Frame(ops) router.Frame(ops)
assertClipboardReadOp(t, router, 0) assertClipboardReadCmd(t, router, 0)
assertClipboardEvent(t, router.Events(&handler[0]), false) assertClipboardEvent(t, router.Events(&handler[0]), false)
ops.Reset() ops.Reset()
} }
@@ -102,7 +113,7 @@ func TestQueueProcessWriteClipboard(t *testing.T) {
router.Source().Queue(clipboard.WriteCmd{Type: mime, Data: io.NopCloser(strings.NewReader("Write 2"))}) router.Source().Queue(clipboard.WriteCmd{Type: mime, Data: io.NopCloser(strings.NewReader("Write 2"))})
router.Frame(ops) router.Frame(ops)
assertClipboardReadOp(t, router, 0) assertClipboardReadCmd(t, router, 0)
assertClipboardWriteCmd(t, router, mime, "Write 2") assertClipboardWriteCmd(t, router, mime, "Write 2")
ops.Reset() ops.Reset()
} }
@@ -112,7 +123,7 @@ func assertClipboardEvent(t *testing.T, events []event.Event, expected bool) {
var evtClipboard int var evtClipboard int
for _, e := range events { for _, e := range events {
switch e.(type) { switch e.(type) {
case clipboard.Event: case transfer.DataEvent:
evtClipboard++ evtClipboard++
} }
} }
@@ -124,7 +135,7 @@ func assertClipboardEvent(t *testing.T, events []event.Event, expected bool) {
} }
} }
func assertClipboardReadOp(t *testing.T, router *Router, expected int) { func assertClipboardReadCmd(t *testing.T, router *Router, expected int) {
t.Helper() t.Helper()
if len(router.cqueue.receivers) != expected { if len(router.cqueue.receivers) != expected {
t.Error("unexpected number of receivers") t.Error("unexpected number of receivers")
@@ -134,7 +145,7 @@ func assertClipboardReadOp(t *testing.T, router *Router, expected int) {
} }
} }
func assertClipboardReadOpDuplicated(t *testing.T, router *Router, expected int) { func assertClipboardReadDuplicated(t *testing.T, router *Router, expected int) {
t.Helper() t.Helper()
if len(router.cqueue.receivers) != expected { if len(router.cqueue.receivers) != expected {
t.Error("receivers removed") t.Error("receivers removed")
+3 -3
View File
@@ -198,7 +198,7 @@ func (q *Router) Queue(events ...event.Event) bool {
if f := q.key.queue.focus; f != nil { if f := q.key.queue.focus; f != nil {
q.handlers.Add(f, e) q.handlers.Add(f, e)
} }
case clipboard.Event: case transfer.DataEvent:
q.cqueue.Push(e, &q.handlers) q.cqueue.Push(e, &q.handlers)
} }
} }
@@ -224,6 +224,8 @@ func (q *Router) executeCommands() {
q.pointer.queue.offerData(req, &q.handlers) q.pointer.queue.offerData(req, &q.handlers)
case clipboard.WriteCmd: case clipboard.WriteCmd:
q.cqueue.ProcessWriteClipboard(req) q.cqueue.ProcessWriteClipboard(req)
case clipboard.ReadCmd:
q.cqueue.ProcessReadClipboard(req.Tag)
} }
} }
q.commands = nil q.commands = nil
@@ -429,8 +431,6 @@ func (q *Router) collect() {
q.wakeup = true q.wakeup = true
q.wakeupTime = op.At q.wakeupTime = op.At
} }
case ops.TypeClipboardRead:
q.cqueue.ProcessReadClipboard(encOp.Refs)
case ops.TypeSave: case ops.TypeSave:
id := ops.DecodeSave(encOp.Data) id := ops.DecodeSave(encOp.Data)
if extra := id - len(q.savedTrans) + 1; extra > 0 { if extra := id - len(q.savedTrans) + 1; extra > 0 {
+7 -3
View File
@@ -21,6 +21,7 @@ import (
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/semantic" "gioui.org/io/semantic"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
"gioui.org/op/clip" "gioui.org/op/clip"
@@ -377,10 +378,13 @@ func (e *Editor) processKey(gtx layout.Context) {
}) })
} }
// Complete a paste event, initiated by Shortcut-V in Editor.command(). // Complete a paste event, initiated by Shortcut-V in Editor.command().
case clipboard.Event: case transfer.DataEvent:
e.scrollCaret = true e.scrollCaret = true
e.scroller.Stop() e.scroller.Stop()
e.Insert(ke.Text) content, err := io.ReadAll(ke.Open())
if err == nil {
e.Insert(string(content))
}
case key.SelectionEvent: case key.SelectionEvent:
e.scrollCaret = true e.scrollCaret = true
e.scroller.Stop() e.scroller.Stop()
@@ -411,7 +415,7 @@ func (e *Editor) command(gtx layout.Context, k key.Event) {
// half is in Editor.processKey() under clipboard.Event. // half is in Editor.processKey() under clipboard.Event.
case "V": case "V":
if !e.ReadOnly { if !e.ReadOnly {
clipboard.ReadOp{Tag: &e.eventKey}.Add(gtx.Ops) gtx.Queue(clipboard.ReadCmd{Tag: &e.eventKey})
} }
// Copy or Cut selection -- ignored if nothing selected. // Copy or Cut selection -- ignored if nothing selected.
case "C", "X": case "C", "X":