From 676b670119fa6b134e0650df36fbf6ae9a726993 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Thu, 19 Oct 2023 17:27:03 -0500 Subject: [PATCH] io/input,io/clipboard: [API] replace ReadOp with command Signed-off-by: Elias Naur --- app/os_android.go | 11 ++++++-- app/os_ios.go | 11 ++++++-- app/os_js.go | 10 ++++++-- app/os_macos.go | 11 ++++++-- app/os_wayland.go | 14 +++++++---- app/os_windows.go | 10 ++++++-- app/os_x11.go | 11 ++++++-- internal/ops/ops.go | 5 ---- io/clipboard/clipboard.go | 21 +++------------- io/input/clipboard.go | 3 +-- io/input/clipboard_test.go | 51 +++++++++++++++++++++++--------------- io/input/router.go | 6 ++--- widget/editor.go | 10 +++++--- 13 files changed, 107 insertions(+), 67 deletions(-) diff --git a/app/os_android.go b/app/os_android.go index c3d196cd..babed7c9 100644 --- a/app/os_android.go +++ b/app/os_android.go @@ -123,12 +123,14 @@ import ( "fmt" "image" "image/color" + "io" "math" "os" "path/filepath" "runtime" "runtime/cgo" "runtime/debug" + "strings" "sync" "time" "unicode/utf16" @@ -137,12 +139,12 @@ import ( "gioui.org/internal/f32color" "gioui.org/f32" - "gioui.org/io/clipboard" "gioui.org/io/input" "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/semantic" "gioui.org/io/system" + "gioui.org/io/transfer" "gioui.org/unit" ) @@ -1311,7 +1313,12 @@ func (w *window) ReadClipboard() { return } 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)) + }, + }) }) } diff --git a/app/os_ios.go b/app/os_ios.go index 7b9d22e2..3d47ee25 100644 --- a/app/os_ios.go +++ b/app/os_ios.go @@ -72,17 +72,19 @@ import "C" import ( "image" + "io" "runtime" "runtime/debug" + "strings" "time" "unicode/utf16" "unsafe" "gioui.org/f32" - "gioui.org/io/clipboard" "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/system" + "gioui.org/io/transfer" "gioui.org/unit" ) @@ -265,7 +267,12 @@ func (w *window) ReadClipboard() { cstr := C.readClipboard() defer C.CFRelease(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) { diff --git a/app/os_js.go b/app/os_js.go index 5b4d39ea..d7ebdd16 100644 --- a/app/os_js.go +++ b/app/os_js.go @@ -6,6 +6,7 @@ import ( "fmt" "image" "image/color" + "io" "strings" "syscall/js" "time" @@ -15,10 +16,10 @@ import ( "gioui.org/internal/f32color" "gioui.org/f32" - "gioui.org/io/clipboard" "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/system" + "gioui.org/io/transfer" "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{} { 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 }) w.addEventListeners() diff --git a/app/os_macos.go b/app/os_macos.go index c06afe9c..94af89fd 100644 --- a/app/os_macos.go +++ b/app/os_macos.go @@ -8,16 +8,18 @@ package app import ( "errors" "image" + "io" "runtime" + "strings" "time" "unicode" "unicode/utf8" "gioui.org/internal/f32" - "gioui.org/io/clipboard" "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/system" + "gioui.org/io/transfer" "gioui.org/unit" _ "gioui.org/internal/cocoainit" @@ -305,7 +307,12 @@ func (w *window) ReadClipboard() { defer C.CFRelease(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) { diff --git a/app/os_wayland.go b/app/os_wayland.go index 13e8fbc6..3c391cc6 100644 --- a/app/os_wayland.go +++ b/app/os_wayland.go @@ -25,10 +25,10 @@ import ( "gioui.org/app/internal/xkb" "gioui.org/f32" "gioui.org/internal/fling" - "gioui.org/io/clipboard" "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/system" + "gioui.org/io/transfer" "gioui.org/unit" ) @@ -209,7 +209,7 @@ type window struct { wsize image.Point // window config size before going fullscreen or maximized inCompositor bool // window is moving or being resized - clipReads chan clipboard.Event + clipReads chan transfer.DataEvent wakeups chan struct{} } @@ -354,7 +354,7 @@ func (d *wlDisplay) createNativeWindow(options []Option) (*window, error) { ppdp: ppdp, ppsp: ppdp, 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) if w.surf == nil { @@ -1021,14 +1021,18 @@ func (w *window) ReadClipboard() { r, err := w.disp.readClipboard() // Send empty responses on unavailable clipboards or errors. if r == nil || err != nil { - w.w.Event(clipboard.Event{}) return } // Don't let slow clipboard transfers block event loop. go func() { defer r.Close() 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() }() } diff --git a/app/os_windows.go b/app/os_windows.go index 20351e61..20fedc34 100644 --- a/app/os_windows.go +++ b/app/os_windows.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "image" + "io" "runtime" "sort" "strings" @@ -22,10 +23,10 @@ import ( gowindows "golang.org/x/sys/windows" "gioui.org/f32" - "gioui.org/io/clipboard" "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/system" + "gioui.org/io/transfer" ) type ViewEvent struct { @@ -667,7 +668,12 @@ func (w *window) readClipboard() error { } defer windows.GlobalUnlock(mem) 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 } diff --git a/app/os_x11.go b/app/os_x11.go index 1ff5f97c..d22674c0 100644 --- a/app/os_x11.go +++ b/app/os_x11.go @@ -30,16 +30,18 @@ import ( "errors" "fmt" "image" + "io" "strconv" + "strings" "sync" "time" "unsafe" "gioui.org/f32" - "gioui.org/io/clipboard" "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/system" + "gioui.org/io/transfer" "gioui.org/unit" syscall "golang.org/x/sys/unix" @@ -650,7 +652,12 @@ func (h *x11EventHandler) handleEvents() bool { break } 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: cevt := (*C.XSelectionRequestEvent)(unsafe.Pointer(xev)) if (cevt.selection != w.atoms.clipboard && cevt.selection != w.atoms.primary) || cevt.property == C.None { diff --git a/internal/ops/ops.go b/internal/ops/ops.go index 2747aae3..20e559f0 100644 --- a/internal/ops/ops.go +++ b/internal/ops/ops.go @@ -63,7 +63,6 @@ const ( TypePass TypePopPass TypePointerInput - TypeClipboardRead TypeSource TypeTarget TypeKeyInput @@ -142,7 +141,6 @@ const ( TypePassLen = 1 TypePopPassLen = 1 TypePointerInputLen = 1 + 1 + 1*2 + 2*4 + 2*4 - TypeClipboardReadLen = 1 TypeSourceLen = 1 TypeTargetLen = 1 TypeKeyInputLen = 1 + 1 @@ -419,7 +417,6 @@ var opProps = [0x100]opProp{ TypePass: {Size: TypePassLen, NumRefs: 0}, TypePopPass: {Size: TypePopPassLen, NumRefs: 0}, TypePointerInput: {Size: TypePointerInputLen, NumRefs: 1}, - TypeClipboardRead: {Size: TypeClipboardReadLen, NumRefs: 1}, TypeSource: {Size: TypeSourceLen, NumRefs: 2}, TypeTarget: {Size: TypeTargetLen, NumRefs: 2}, TypeKeyInput: {Size: TypeKeyInputLen, NumRefs: 2}, @@ -484,8 +481,6 @@ func (t OpType) String() string { return "PopPass" case TypePointerInput: return "PointerInput" - case TypeClipboardRead: - return "ClipboardRead" case TypeSource: return "Source" case TypeTarget: diff --git a/io/clipboard/clipboard.go b/io/clipboard/clipboard.go index 62b99906..1eec9467 100644 --- a/io/clipboard/clipboard.go +++ b/io/clipboard/clipboard.go @@ -5,33 +5,20 @@ package clipboard import ( "io" - "gioui.org/internal/ops" "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. type WriteCmd struct { Type string Data io.ReadCloser } -// ReadOp requests the text of the clipboard, delivered to -// the current handler through an Event. -type ReadOp struct { +// ReadCmd requests the text of the clipboard, delivered to +// the handler through an [io/transfer.DataEvent]. +type ReadCmd struct { 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 (ReadCmd) ImplementsCommand() {} diff --git a/io/input/clipboard.go b/io/input/clipboard.go index 5d881d97..2defc67e 100644 --- a/io/input/clipboard.go +++ b/io/input/clipboard.go @@ -55,11 +55,10 @@ func (q *clipboardQueue) ProcessWriteClipboard(req clipboard.WriteCmd) { q.text = content } -func (q *clipboardQueue) ProcessReadClipboard(refs []interface{}) { +func (q *clipboardQueue) ProcessReadClipboard(tag event.Tag) { if q.receivers == nil { q.receivers = make(map[event.Tag]struct{}) } - tag := refs[0].(event.Tag) if _, ok := q.receivers[tag]; !ok { q.receivers[tag] = struct{}{} q.requested = false diff --git a/io/input/clipboard_test.go b/io/input/clipboard_test.go index 378c787c..626fda93 100644 --- a/io/input/clipboard_test.go +++ b/io/input/clipboard_test.go @@ -9,6 +9,7 @@ import ( "gioui.org/io/clipboard" "gioui.org/io/event" + "gioui.org/io/transfer" "gioui.org/op" ) @@ -16,30 +17,35 @@ func TestClipboardDuplicateEvent(t *testing.T) { ops, router, handler := new(op.Ops), new(Router), make([]int, 2) // Both must receive the event once - clipboard.ReadOp{Tag: &handler[0]}.Add(ops) - clipboard.ReadOp{Tag: &handler[1]}.Add(ops) + router.Source().Queue(clipboard.ReadCmd{Tag: &handler[0]}) + router.Source().Queue(clipboard.ReadCmd{Tag: &handler[1]}) 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) - assertClipboardReadOp(t, router, 0) + assertClipboardReadCmd(t, router, 0) assertClipboardEvent(t, router.Events(&handler[0]), true) assertClipboardEvent(t, router.Events(&handler[1]), true) ops.Reset() - // No ReadOp + // No ReadCmd router.Frame(ops) - assertClipboardReadOp(t, router, 0) + assertClipboardReadCmd(t, router, 0) assertClipboardEvent(t, router.Events(&handler[0]), false) assertClipboardEvent(t, router.Events(&handler[1]), false) ops.Reset() - clipboard.ReadOp{Tag: &handler[0]}.Add(ops) + router.Source().Queue(clipboard.ReadCmd{Tag: &handler[0]}) router.Frame(ops) // No ClipboardEvent sent - assertClipboardReadOp(t, router, 1) + assertClipboardReadCmd(t, router, 1) assertClipboardEvent(t, router.Events(&handler[0]), false) assertClipboardEvent(t, router.Events(&handler[1]), false) ops.Reset() @@ -50,34 +56,39 @@ func TestQueueProcessReadClipboard(t *testing.T) { ops.Reset() // Request read - clipboard.ReadOp{Tag: &handler[0]}.Add(ops) + router.Source().Queue(clipboard.ReadCmd{Tag: &handler[0]}) router.Frame(ops) - assertClipboardReadOp(t, router, 1) + assertClipboardReadCmd(t, router, 1) ops.Reset() for i := 0; i < 3; i++ { - // No ReadOp + // No ReadCmd // One receiver must still wait for response router.Frame(ops) - assertClipboardReadOpDuplicated(t, router, 1) + assertClipboardReadDuplicated(t, router, 1) ops.Reset() } router.Frame(ops) // 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) - assertClipboardReadOp(t, router, 0) + assertClipboardReadCmd(t, router, 0) assertClipboardEvent(t, router.Events(&handler[0]), true) ops.Reset() - // No ReadOp + // No ReadCmd // There's no receiver waiting router.Frame(ops) - assertClipboardReadOp(t, router, 0) + assertClipboardReadCmd(t, router, 0) assertClipboardEvent(t, router.Events(&handler[0]), false) 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.Frame(ops) - assertClipboardReadOp(t, router, 0) + assertClipboardReadCmd(t, router, 0) assertClipboardWriteCmd(t, router, mime, "Write 2") ops.Reset() } @@ -112,7 +123,7 @@ func assertClipboardEvent(t *testing.T, events []event.Event, expected bool) { var evtClipboard int for _, e := range events { switch e.(type) { - case clipboard.Event: + case transfer.DataEvent: 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() if len(router.cqueue.receivers) != expected { 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() if len(router.cqueue.receivers) != expected { t.Error("receivers removed") diff --git a/io/input/router.go b/io/input/router.go index 82786b67..7772ddad 100644 --- a/io/input/router.go +++ b/io/input/router.go @@ -198,7 +198,7 @@ func (q *Router) Queue(events ...event.Event) bool { if f := q.key.queue.focus; f != nil { q.handlers.Add(f, e) } - case clipboard.Event: + case transfer.DataEvent: q.cqueue.Push(e, &q.handlers) } } @@ -224,6 +224,8 @@ func (q *Router) executeCommands() { q.pointer.queue.offerData(req, &q.handlers) case clipboard.WriteCmd: q.cqueue.ProcessWriteClipboard(req) + case clipboard.ReadCmd: + q.cqueue.ProcessReadClipboard(req.Tag) } } q.commands = nil @@ -429,8 +431,6 @@ func (q *Router) collect() { q.wakeup = true q.wakeupTime = op.At } - case ops.TypeClipboardRead: - q.cqueue.ProcessReadClipboard(encOp.Refs) case ops.TypeSave: id := ops.DecodeSave(encOp.Data) if extra := id - len(q.savedTrans) + 1; extra > 0 { diff --git a/widget/editor.go b/widget/editor.go index 952c162a..4ccf1b8a 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -21,6 +21,7 @@ import ( "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" @@ -377,10 +378,13 @@ func (e *Editor) processKey(gtx layout.Context) { }) } // Complete a paste event, initiated by Shortcut-V in Editor.command(). - case clipboard.Event: + case transfer.DataEvent: e.scrollCaret = true e.scroller.Stop() - e.Insert(ke.Text) + content, err := io.ReadAll(ke.Open()) + if err == nil { + e.Insert(string(content)) + } case key.SelectionEvent: e.scrollCaret = true 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. case "V": 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. case "C", "X":