~eliasnaur/gio-patches

gio: io,app: add ReadClipboardOp and WriteClipboardOp v2 v1 PROPOSED

~inkeliz
~inkeliz: 1
 io,app: add ReadClipboardOp and WriteClipboardOp v2

 13 files changed, 177 insertions(+), 17 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~eliasnaur/gio-patches/patches/15402/mbox | git am -3
Learn more about email & git
View this thread in the archives

[PATCH gio] io,app: add ReadClipboardOp and WriteClipboardOp v2 Export this patch

~inkeliz
From: Inkeliz <inkeliz@inkeliz.com>

Previously, the only way to manipulate the clipboard (read or write) is
using the `app.Window`, that is inaccessible inside widgets, by default.

The new `clipboard.ReadClipboardOp` and `clipboard.WriteClipboardOp`
makes possible to read/write inside the widget. The old
`system.ClipboardEvent` was removed and replaced by `clipboard.Event`.

Fixes gio#183.

Signed-off-by: Inkeliz <inkeliz@inkeliz.com>
---
 app/internal/window/os_android.go |  3 +-
 app/internal/window/os_ios.go     |  3 +-
 app/internal/window/os_js.go      |  3 +-
 app/internal/window/os_macos.go   |  3 +-
 app/internal/window/os_wayland.go |  5 +-
 app/internal/window/os_windows.go |  3 +-
 app/internal/window/os_x11.go     |  3 +-
 app/window.go                     | 12 +++-
 internal/opconst/ops.go           |  8 ++-
 io/clipboard/clipboard.go         | 34 +++++++++++
 io/router/clipboard.go            | 93 +++++++++++++++++++++++++++++++
 io/router/router.go               | 17 ++++++
 io/system/system.go               |  7 ---
 13 files changed, 177 insertions(+), 17 deletions(-)
 create mode 100644 io/clipboard/clipboard.go
 create mode 100644 io/router/clipboard.go

diff --git a/app/internal/window/os_android.go b/app/internal/window/os_android.go
index fc494e5..b73ca38 100644
--- a/app/internal/window/os_android.go
+++ b/app/internal/window/os_android.go
@@ -52,6 +52,7 @@ import (
	"unsafe"

	"gioui.org/f32"
	"gioui.org/io/clipboard"
	"gioui.org/io/key"
	"gioui.org/io/pointer"
	"gioui.org/io/system"
@@ -645,7 +646,7 @@ func (w *window) ReadClipboard() {
			return
		}
		content := goString(env, C.jstring(c))
		w.callbacks.Event(system.ClipboardEvent{Text: content})
		w.callbacks.Event(clipboard.Event{Text: content})
	})
}

diff --git a/app/internal/window/os_ios.go b/app/internal/window/os_ios.go
index 1865454..d92999a 100644
--- a/app/internal/window/os_ios.go
+++ b/app/internal/window/os_ios.go
@@ -38,6 +38,7 @@ import (
	"unsafe"

	"gioui.org/f32"
	"gioui.org/io/clipboard"
	"gioui.org/io/key"
	"gioui.org/io/pointer"
	"gioui.org/io/system"
@@ -222,7 +223,7 @@ func onTouch(last C.int, view, touchRef C.CFTypeRef, phase C.NSInteger, x, y C.C
func (w *window) ReadClipboard() {
	runOnMain(func() {
		content := nsstringToString(C.gio_readClipboard())
		w.w.Event(system.ClipboardEvent{Text: content})
		w.w.Event(clipboard.Event{Text: content})
	})
}

diff --git a/app/internal/window/os_js.go b/app/internal/window/os_js.go
index e4fb17c..3c26922 100644
--- a/app/internal/window/os_js.go
+++ b/app/internal/window/os_js.go
@@ -12,6 +12,7 @@ import (
	"unicode/utf8"

	"gioui.org/f32"
	"gioui.org/io/clipboard"
	"gioui.org/io/key"
	"gioui.org/io/pointer"
	"gioui.org/io/system"
@@ -59,7 +60,7 @@ func NewWindow(win Callbacks, opts *Options) error {
	})
	w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} {
		content := args[0].String()
		win.Event(system.ClipboardEvent{Text: content})
		win.Event(clipboard.Event{Text: content})
		return nil
	})
	w.addEventListeners()
diff --git a/app/internal/window/os_macos.go b/app/internal/window/os_macos.go
index 1afec6f..339e659 100644
--- a/app/internal/window/os_macos.go
+++ b/app/internal/window/os_macos.go
@@ -14,6 +14,7 @@ import (
	"unsafe"

	"gioui.org/f32"
	"gioui.org/io/clipboard"
	"gioui.org/io/key"
	"gioui.org/io/pointer"
	"gioui.org/io/system"
@@ -107,7 +108,7 @@ func (w *window) contextView() C.CFTypeRef {
func (w *window) ReadClipboard() {
	runOnMain(func() {
		content := nsstringToString(C.gio_readClipboard())
		w.w.Event(system.ClipboardEvent{Text: content})
		w.w.Event(clipboard.Event{Text: content})
	})
}

diff --git a/app/internal/window/os_wayland.go b/app/internal/window/os_wayland.go
index 60f59b1..36837b1 100644
--- a/app/internal/window/os_wayland.go
+++ b/app/internal/window/os_wayland.go
@@ -22,6 +22,7 @@ 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"
@@ -1099,14 +1100,14 @@ func (w *window) process() {
		r, err := w.disp.readClipboard()
		// Send empty responses on unavailable clipboards or errors.
		if r == nil || err != nil {
			w.w.Event(system.ClipboardEvent{})
			w.w.Event(clipboard.Event{})
			return
		}
		// Don't let slow clipboard transfers block event loop.
		go func() {
			defer r.Close()
			data, _ := ioutil.ReadAll(r)
			w.w.Event(system.ClipboardEvent{Text: string(data)})
			w.w.Event(clipboard.Event{Text: string(data)})
		}()
	}
	if writeClipboard != nil {
diff --git a/app/internal/window/os_windows.go b/app/internal/window/os_windows.go
index 2a38d53..041c276 100644
--- a/app/internal/window/os_windows.go
+++ b/app/internal/window/os_windows.go
@@ -22,6 +22,7 @@ import (
	"gioui.org/unit"

	"gioui.org/f32"
	"gioui.org/io/clipboard"
	"gioui.org/io/key"
	"gioui.org/io/pointer"
	"gioui.org/io/system"
@@ -508,7 +509,7 @@ func (w *window) readClipboard() error {
	hdr.Len = n
	content := string(utf16.Decode(u16))
	go func() {
		w.w.Event(system.ClipboardEvent{Text: content})
		w.w.Event(clipboard.Event{Text: content})
	}()
	return nil
}
diff --git a/app/internal/window/os_x11.go b/app/internal/window/os_x11.go
index 413a959..4317386 100644
--- a/app/internal/window/os_x11.go
+++ b/app/internal/window/os_x11.go
@@ -34,6 +34,7 @@ import (
	"unsafe"

	"gioui.org/f32"
	"gioui.org/io/clipboard"
	"gioui.org/io/key"
	"gioui.org/io/pointer"
	"gioui.org/io/system"
@@ -410,7 +411,7 @@ func (h *x11EventHandler) handleEvents() bool {
				break
			}
			str := C.GoStringN((*C.char)(unsafe.Pointer(text.value)), C.int(text.nitems))
			w.w.Event(system.ClipboardEvent{Text: str})
			w.w.Event(clipboard.Event{Text: str})
		case C.SelectionRequest:
			cevt := (*C.XSelectionRequestEvent)(unsafe.Pointer(xev))
			if cevt.selection != w.atoms.clipboard || cevt.property == C.None {
diff --git a/app/window.go b/app/window.go
index ed46958..99fa89e 100644
--- a/app/window.go
+++ b/app/window.go
@@ -167,6 +167,16 @@ func (w *Window) processFrame(frameStart time.Time, size image.Point, frame *op.
	case router.TextInputClose:
		w.driver.ShowTextInput(false)
	}
	if w.queue.q.ReadClipboard() {
		go func() {
			w.driver.ReadClipboard()
		}()
	}
	if text := w.queue.q.WriteClipboard(); text != nil {
		go func() {
			w.driver.WriteClipboard(*text)
		}()
	}
	if w.queue.q.Profiling() {
		frameDur := time.Since(frameStart)
		frameDur = frameDur.Truncate(100 * time.Microsecond)
@@ -194,7 +204,7 @@ func (w *Window) Invalidate() {
}

// ReadClipboard initiates a read of the clipboard in the form
// of a system.ClipboardEvent. Multiple reads may be coalesced
// of a clipboard.Event. Multiple reads may be coalesced
// to a single event.
func (w *Window) ReadClipboard() {
	go w.driverDo(func() {
diff --git a/internal/opconst/ops.go b/internal/opconst/ops.go
index dba0f54..3634ce9 100644
--- a/internal/opconst/ops.go
+++ b/internal/opconst/ops.go
@@ -20,6 +20,8 @@ const (
	TypeArea
	TypePointerInput
	TypePass
	TypeWriteClipboard
	TypeReadClipboard
	TypeKeyInput
	TypeKeyFocus
	TypeKeySoftKeyboard
@@ -43,6 +45,8 @@ const (
	TypeAreaLen            = 1 + 1 + 4*4
	TypePointerInputLen    = 1 + 1 + 1
	TypePassLen            = 1 + 1
	TypeWriteClipboardLen  = 1
	TypeReadClipboardLen   = 1
	TypeKeyInputLen        = 1
	TypeKeyFocusLen        = 1 + 1
	TypeKeySoftKeyboardLen = 1 + 1
@@ -67,6 +71,8 @@ func (t OpType) Size() int {
		TypeAreaLen,
		TypePointerInputLen,
		TypePassLen,
		TypeWriteClipboardLen,
		TypeReadClipboardLen,
		TypeKeyInputLen,
		TypeKeyFocusLen,
		TypeKeySoftKeyboardLen,
@@ -80,7 +86,7 @@ func (t OpType) Size() int {

func (t OpType) NumRefs() int {
	switch t {
	case TypeKeyInput, TypePointerInput, TypeProfile, TypeCall:
	case TypeKeyInput, TypePointerInput, TypeProfile, TypeCall, TypeReadClipboard, TypeWriteClipboard:
		return 1
	case TypeImage:
		return 2
diff --git a/io/clipboard/clipboard.go b/io/clipboard/clipboard.go
new file mode 100644
index 0000000..46d6b00
--- /dev/null
+++ b/io/clipboard/clipboard.go
@@ -0,0 +1,34 @@
package clipboard

import (
	"gioui.org/internal/opconst"
	"gioui.org/io/event"
	"gioui.org/op"
)

type ReadClipboardOp struct {
	Tag event.Tag
}

type WriteClipboardOp struct {
	Text string
}

// Event is generated when a handler request
// the ReadClipboardOp
type Event struct {
	Text string
}

func (h ReadClipboardOp) Add(o *op.Ops) {
	data := o.Write1(opconst.TypeReadClipboardLen, h.Tag)
	data[0] = byte(opconst.TypeReadClipboard)
}

func (h WriteClipboardOp) Add(o *op.Ops) {
	data := o.Write1(opconst.TypeWriteClipboardLen, &h.Text)
	data[0] = byte(opconst.TypeWriteClipboard)
}


func (Event) ImplementsEvent()  {}
\ No newline at end of file
diff --git a/io/router/clipboard.go b/io/router/clipboard.go
new file mode 100644
index 0000000..06aa768
--- /dev/null
+++ b/io/router/clipboard.go
@@ -0,0 +1,93 @@
package router

import (
	"gioui.org/internal/opconst"
	"gioui.org/internal/ops"
	"gioui.org/io/event"
	"gioui.org/op"
)

type clipboardQueue struct {
	receiver  event.Tag
	requested bool
	text      *string
	reader    ops.Reader
}

// WriteClipboard returns the last text supossed to be
// copied to clipboard as determined in Frame.
func (q *clipboardQueue) WriteClipboard() *string {
	if q.text != nil {
		t := q.text
		q.text = nil
		return t
	}
	return nil
}

// ReadClipboard returns true if there's any request
// to read the clipboard.
func (q *clipboardQueue) ReadClipboard() bool {
	if q.receiver != nil && !q.requested {
		q.requested = true
		return true
	}
	return false
}

func (q *clipboardQueue) Frame(root *op.Ops, events *handlerEvents) {
	q.reader.Reset(root)

	receiver, text := q.resolveClipboard(events)
	if text != nil {
		q.text = text
	}
	if receiver != nil {
		q.receiver = receiver
		q.requested = false
	}
}

func (q *clipboardQueue) Push(e event.Event, events *handlerEvents) {
	if q.receiver != nil {
		events.Add(q.receiver, e)
		q.receiver = nil
	}
}

func (q *clipboardQueue) resolveClipboard(events *handlerEvents) (receiver event.Tag, text *string) {
loop:
	for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
		switch opconst.OpType(encOp.Data[0]) {
		case opconst.TypeWriteClipboard:
			text = decodeWriteClipboard(encOp.Data, encOp.Refs)
		case opconst.TypeReadClipboard:
			receiver = decodeReadClipboard(encOp.Data, encOp.Refs)
		case opconst.TypePush:
			newReceiver, newWrite := q.resolveClipboard(events)
			if newWrite != nil {
				text = newWrite
			}
			if newReceiver != nil {
				receiver = newReceiver
			}
		case opconst.TypePop:
			break loop
		}
	}
	return receiver, text
}

func decodeWriteClipboard(d []byte, refs []interface{}) *string {
	if opconst.OpType(d[0]) != opconst.TypeWriteClipboard {
		panic("invalid op")
	}
	return refs[0].(*string)
}

func decodeReadClipboard(d []byte, refs []interface{}) event.Tag {
	if opconst.OpType(d[0]) != opconst.TypeReadClipboard {
		panic("invalid op")
	}
	return refs[0].(event.Tag)
}
diff --git a/io/router/router.go b/io/router/router.go
index d24d214..7ab212d 100644
--- a/io/router/router.go
+++ b/io/router/router.go
@@ -16,6 +16,7 @@ import (

	"gioui.org/internal/opconst"
	"gioui.org/internal/ops"
	"gioui.org/io/clipboard"
	"gioui.org/io/event"
	"gioui.org/io/key"
	"gioui.org/io/pointer"
@@ -28,6 +29,7 @@ import (
type Router struct {
	pqueue pointerQueue
	kqueue keyQueue
	cqueue clipboardQueue

	handlers handlerEvents

@@ -73,6 +75,7 @@ func (q *Router) Frame(ops *op.Ops) {

	q.pqueue.Frame(ops, &q.handlers)
	q.kqueue.Frame(ops, &q.handlers)
	q.cqueue.Frame(ops, &q.handlers)
	if q.handlers.HadEvents() {
		q.wakeup = true
		q.wakeupTime = time.Time{}
@@ -88,6 +91,8 @@ func (q *Router) Add(events ...event.Event) bool {
			q.pqueue.Push(e, &q.handlers)
		case key.EditEvent, key.Event, key.FocusEvent:
			q.kqueue.Push(e, &q.handlers)
		case clipboard.Event:
			q.cqueue.Push(e, &q.handlers)
		}
	}
	return q.handlers.HadEvents()
@@ -99,6 +104,18 @@ func (q *Router) TextInputState() TextInputState {
	return q.kqueue.InputState()
}

// WriteClipboard returns the most recent text to be copied
// to the clipboard, if any.
func (q *Router) WriteClipboard() *string {
	return q.cqueue.WriteClipboard()
}

// ReadClipboard returns true if some handler is waiting to
// retrieve the clipboard.
func (q *Router) ReadClipboard() bool {
	return q.cqueue.ReadClipboard()
}

func (q *Router) collect() {
	for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
		switch opconst.OpType(encOp.Data[0]) {
diff --git a/io/system/system.go b/io/system/system.go
index 4aa88d3..8a84051 100644
--- a/io/system/system.go
+++ b/io/system/system.go
@@ -60,12 +60,6 @@ type DestroyEvent struct {
	Err error
}

// ClipboardEvent is sent once for each request for the
// clipboard content.
type ClipboardEvent struct {
	Text string
}

// Insets is the space taken up by
// system decoration such as translucent
// system bars and software keyboards.
@@ -122,4 +116,3 @@ func (FrameEvent) ImplementsEvent()     {}
func (StageEvent) ImplementsEvent()     {}
func (*CommandEvent) ImplementsEvent()  {}
func (DestroyEvent) ImplementsEvent()   {}
func (ClipboardEvent) ImplementsEvent() {}
-- 
2.26.2
If you're going to add package clipboard, please split into a separate
change the new package and adjusted references. Note that the change is
and API change in the commit message, and include an automatic gofmt fixup
if possible. See gioui.org/commit/878131189b10c2341 for an example.

On Tue Dec 1, 2020 at 04:37, ~inkeliz wrote: