io/input,io/transfer: [API] replace OfferOp with command

Also delete two tests that are no longer relevant.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2023-10-09 17:38:53 -05:00
parent eed93aaffe
commit a3c539b3c2
8 changed files with 73 additions and 221 deletions
-5
View File
@@ -67,7 +67,6 @@ const (
TypeClipboardWrite TypeClipboardWrite
TypeSource TypeSource
TypeTarget TypeTarget
TypeOffer
TypeKeyInput TypeKeyInput
TypeSave TypeSave
TypeLoad TypeLoad
@@ -148,7 +147,6 @@ const (
TypeClipboardWriteLen = 1 TypeClipboardWriteLen = 1
TypeSourceLen = 1 TypeSourceLen = 1
TypeTargetLen = 1 TypeTargetLen = 1
TypeOfferLen = 1
TypeKeyInputLen = 1 + 1 TypeKeyInputLen = 1 + 1
TypeSaveLen = 1 + 4 TypeSaveLen = 1 + 4
TypeLoadLen = 1 + 4 TypeLoadLen = 1 + 4
@@ -427,7 +425,6 @@ var opProps = [0x100]opProp{
TypeClipboardWrite: {Size: TypeClipboardWriteLen, NumRefs: 1}, TypeClipboardWrite: {Size: TypeClipboardWriteLen, NumRefs: 1},
TypeSource: {Size: TypeSourceLen, NumRefs: 2}, TypeSource: {Size: TypeSourceLen, NumRefs: 2},
TypeTarget: {Size: TypeTargetLen, NumRefs: 2}, TypeTarget: {Size: TypeTargetLen, NumRefs: 2},
TypeOffer: {Size: TypeOfferLen, NumRefs: 3},
TypeKeyInput: {Size: TypeKeyInputLen, NumRefs: 2}, TypeKeyInput: {Size: TypeKeyInputLen, NumRefs: 2},
TypeSave: {Size: TypeSaveLen, NumRefs: 0}, TypeSave: {Size: TypeSaveLen, NumRefs: 0},
TypeLoad: {Size: TypeLoadLen, NumRefs: 0}, TypeLoad: {Size: TypeLoadLen, NumRefs: 0},
@@ -498,8 +495,6 @@ func (t OpType) String() string {
return "Source" return "Source"
case TypeTarget: case TypeTarget:
return "Target" return "Target"
case TypeOffer:
return "Offer"
case TypeKeyInput: case TypeKeyInput:
return "KeyInput" return "KeyInput"
case TypeSave: case TypeSave:
+35 -41
View File
@@ -72,8 +72,6 @@ type pointerHandler struct {
sourceMimes []string sourceMimes []string
targetMimes []string targetMimes []string
offeredMime string
data io.ReadCloser
} }
type areaOp struct { type areaOp struct {
@@ -236,16 +234,23 @@ func (c *pointerCollector) newHandler(tag event.Tag, events *handlerEvents) *poi
tag: tag, tag: tag,
pass: c.state.pass > 0, pass: c.state.pass > 0,
}) })
h, ok := c.q.handlers[tag] h := c.q.handlerFor(tag, events)
h.area = areaID
return h
}
func (q *pointerQueue) handlerFor(tag event.Tag, events *handlerEvents) *pointerHandler {
h, ok := q.handlers[tag]
if !ok { if !ok {
h = new(pointerHandler) h = &pointerHandler{
c.q.handlers[tag] = h area: -1,
}
q.handlers[tag] = h
// Cancel handlers on (each) first appearance, but don't // Cancel handlers on (each) first appearance, but don't
// trigger redraw. // trigger redraw.
events.AddNoRedraw(tag, pointer.Event{Kind: pointer.Cancel}) events.AddNoRedraw(tag, pointer.Event{Kind: pointer.Cancel})
} }
h.active = true h.active = true
h.area = areaID
return h return h
} }
@@ -332,10 +337,27 @@ func (c *pointerCollector) targetOp(op transfer.TargetOp, events *handlerEvents)
h.targetMimes = append(h.targetMimes, op.Type) h.targetMimes = append(h.targetMimes, op.Type)
} }
func (c *pointerCollector) offerOp(op transfer.OfferOp, events *handlerEvents) { func (q *pointerQueue) offerData(req transfer.OfferCmd, events *handlerEvents) {
h := c.newHandler(op.Tag, events) transferIdx := len(q.transfers)
h.offeredMime = op.Type q.transfers = append(q.transfers, req.Data)
h.data = op.Data for i := range q.pointers {
p := &q.pointers[i]
if p.dataSource != req.Tag {
continue
}
defer q.deliverTransferCancelEvent(p, events)
if p.dataTarget == nil {
return
}
events.Add(p.dataTarget, transfer.DataEvent{
Type: req.Type,
Open: func() io.ReadCloser {
q.transfers[transferIdx] = nil
return req.Data
},
})
break
}
} }
func (c *pointerCollector) reset() { func (c *pointerCollector) reset() {
@@ -537,6 +559,7 @@ func (q *pointerQueue) reset() {
for _, h := range q.handlers { for _, h := range q.handlers {
// Reset handler. // Reset handler.
h.active = false h.active = false
h.area = -1
h.wantsGrab = false h.wantsGrab = false
h.types = 0 h.types = 0
h.sourceMimes = h.sourceMimes[:0] h.sourceMimes = h.sourceMimes[:0]
@@ -596,7 +619,6 @@ func (q *pointerQueue) Frame(events *handlerEvents) {
for i := range q.pointers { for i := range q.pointers {
p := &q.pointers[i] p := &q.pointers[i]
q.deliverEnterLeaveEvents(p, events, p.last) q.deliverEnterLeaveEvents(p, events, p.last)
q.deliverTransferDataEvent(p, events)
} }
} }
@@ -790,10 +812,10 @@ func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEv
continue continue
} }
h := q.handlers[k] h := q.handlers[k]
e := e
e.Kind = pointer.Leave e.Kind = pointer.Leave
if e.Kind&h.types != 0 { if e.Kind&h.types != 0 {
e := e
e.Position = q.invTransform(h.area, e.Position) e.Position = q.invTransform(h.area, e.Position)
events.Add(k, e) events.Add(k, e)
} }
@@ -804,10 +826,10 @@ func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEv
if _, found := searchTag(p.entered, k); found { if _, found := searchTag(p.entered, k); found {
continue continue
} }
e := e
e.Kind = pointer.Enter e.Kind = pointer.Enter
if e.Kind&h.types != 0 { if e.Kind&h.types != 0 {
e := e
e.Position = q.invTransform(h.area, e.Position) e.Position = q.invTransform(h.area, e.Position)
events.Add(k, e) events.Add(k, e)
} }
@@ -855,32 +877,6 @@ func (q *pointerQueue) deliverDropEvent(p *pointerInfo, events *handlerEvents) {
q.deliverTransferCancelEvent(p, events) q.deliverTransferCancelEvent(p, events)
} }
func (q *pointerQueue) deliverTransferDataEvent(p *pointerInfo, events *handlerEvents) {
if p.dataSource == nil {
return
}
src := q.handlers[p.dataSource]
if src.data == nil {
// Data not received yet.
return
}
if p.dataTarget == nil {
q.deliverTransferCancelEvent(p, events)
return
}
// Send the offered data to the target.
transferIdx := len(q.transfers)
events.Add(p.dataTarget, transfer.DataEvent{
Type: src.offeredMime,
Open: func() io.ReadCloser {
q.transfers[transferIdx] = nil
return src.data
},
})
q.transfers = append(q.transfers, src.data)
p.dataTarget = nil
}
func (q *pointerQueue) deliverTransferCancelEvent(p *pointerInfo, events *handlerEvents) { func (q *pointerQueue) deliverTransferCancelEvent(p *pointerInfo, events *handlerEvents) {
events.Add(p.dataSource, transfer.CancelEvent{}) events.Add(p.dataSource, transfer.CancelEvent{})
// Cancel all potential targets. // Cancel all potential targets.
@@ -890,8 +886,6 @@ func (q *pointerQueue) deliverTransferCancelEvent(p *pointerInfo, events *handle
events.Add(k, transfer.CancelEvent{}) events.Add(k, transfer.CancelEvent{})
} }
} }
src.offeredMime = ""
src.data = nil
p.dataSource = nil p.dataSource = nil
p.dataTarget = nil p.dataTarget = nil
} }
+9 -128
View File
@@ -821,15 +821,6 @@ func TestTransfer(t *testing.T) {
// Cancel is received when the pointer is first seen. // Cancel is received when the pointer is first seen.
cancel := pointer.Event{Kind: pointer.Cancel} cancel := pointer.Event{Kind: pointer.Cancel}
t.Run("transfer.Offer should panic on nil Data", func(t *testing.T) {
defer func() {
if recover() == nil {
t.Error("expected panic upon invalid data")
}
}()
transfer.OfferOp{}.Add(new(op.Ops))
})
t.Run("drop on no target", func(t *testing.T) { t.Run("drop on no target", func(t *testing.T) {
ops := new(op.Ops) ops := new(op.Ops)
src, tgt := setup(ops, "file", "file") src, tgt := setup(ops, "file", "file")
@@ -959,16 +950,13 @@ func TestTransfer(t *testing.T) {
// Offer valid type and data. // Offer valid type and data.
ofr := &offer{data: "hello"} ofr := &offer{data: "hello"}
transfer.OfferOp{ r.Source().Queue(transfer.OfferCmd{Tag: src, Type: "file", Data: ofr})
Tag: src,
Type: "file",
Data: ofr,
}.Add(ops)
r.Frame(ops) r.Frame(ops)
evs := r.Events(tgt) evs := r.Events(tgt)
if len(evs) != 1 { if len(evs) != 2 {
t.Fatalf("unexpected number of events: %d, want 1", len(evs)) t.Fatalf("unexpected number of events: %d, want 2", len(evs))
} }
assertEventSequence(t, evs[1:], transfer.CancelEvent{})
dataEvent, ok := evs[0].(transfer.DataEvent) dataEvent, ok := evs[0].(transfer.DataEvent)
if !ok { if !ok {
t.Fatalf("unexpected event type: %T, want %T", dataEvent, transfer.DataEvent{}) t.Fatalf("unexpected event type: %T, want %T", dataEvent, transfer.DataEvent{})
@@ -984,9 +972,8 @@ func TestTransfer(t *testing.T) {
if ofr.closed { if ofr.closed {
t.Error("offer closed prematurely") t.Error("offer closed prematurely")
} }
r.Frame(ops)
assertEventSequence(t, r.Events(src), transfer.CancelEvent{}) assertEventSequence(t, r.Events(src), transfer.CancelEvent{})
assertEventSequence(t, r.Events(tgt), transfer.CancelEvent{}) r.Frame(ops)
}) })
t.Run("drop on valid target, DataEvent not used", func(t *testing.T) { t.Run("drop on valid target, DataEvent not used", func(t *testing.T) {
@@ -1017,122 +1004,16 @@ func TestTransfer(t *testing.T) {
}, },
) )
ofr := &offer{data: "hello"} ofr := &offer{data: "hello"}
transfer.OfferOp{ r.Source().Queue(transfer.OfferCmd{Tag: src, Type: "file", Data: ofr})
Tag: src,
Type: "file",
Data: ofr,
}.Add(ops)
r.Frame(ops)
// DataEvent should be used here. The next frame should close it as unused.
r.Frame(ops) r.Frame(ops)
assertEventSequence(t, r.Events(src), transfer.CancelEvent{}) assertEventSequence(t, r.Events(src), transfer.CancelEvent{})
assertEventSequence(t, r.Events(tgt), transfer.CancelEvent{}) // Ignore DataEvent and verify that the next frame closes it as unused.
assertEventSequence(t, r.Events(tgt)[1:], transfer.CancelEvent{})
r.Frame(ops)
if !ofr.closed { if !ofr.closed {
t.Error("offer was not closed") t.Error("offer was not closed")
} }
}) })
t.Run("valid target enter/leave events", func(t *testing.T) {
ops := new(op.Ops)
src, _ := setup(ops, "file", "file")
pass := pointer.PassOp{}.Push(ops)
stack := clip.Rect(tgtArea).Push(ops)
tag := new(int)
pointer.InputOp{
Tag: tag,
Kinds: pointer.Enter | pointer.Leave,
}.Add(ops)
stack.Pop()
pass.Pop()
var r Router
r.Frame(ops)
// Drag.
r.Queue(
pointer.Event{
Position: f32.Pt(10, 10),
Kind: pointer.Press,
},
pointer.Event{
Position: f32.Pt(10, 10),
Kind: pointer.Move,
},
pointer.Event{
Position: f32.Pt(40, 10),
Kind: pointer.Move,
},
)
assertEventPointerTypeSequence(t, r.Events(tag), pointer.Cancel, pointer.Enter)
// Drop.
r.Queue(
pointer.Event{
Position: f32.Pt(40, 10),
Kind: pointer.Release,
},
)
// Offer valid type and data.
ofr := &offer{data: "hello"}
transfer.OfferOp{
Tag: src,
Type: "file",
Data: ofr,
}.Add(ops)
r.Frame(ops)
assertEventPointerTypeSequence(t, r.Events(tag), pointer.Leave)
})
t.Run("invalid target NO enter/leave events", func(t *testing.T) {
ops := new(op.Ops)
src, _ := setup(ops, "file", "nofile")
pass := pointer.PassOp{}.Push(ops)
stack := clip.Rect(tgtArea).Push(ops)
tag := new(int)
pointer.InputOp{
Tag: tag,
Kinds: pointer.Enter | pointer.Leave,
}.Add(ops)
stack.Pop()
pass.Pop()
var r Router
r.Frame(ops)
// Drag.
r.Queue(
pointer.Event{
Position: f32.Pt(10, 10),
Kind: pointer.Press,
},
pointer.Event{
Position: f32.Pt(10, 10),
Kind: pointer.Move,
},
pointer.Event{
Position: f32.Pt(40, 10),
Kind: pointer.Move,
},
)
assertEventPointerTypeSequence(t, r.Events(tag), pointer.Cancel)
// Drop.
r.Queue(
pointer.Event{
Position: f32.Pt(40, 10),
Kind: pointer.Release,
},
)
// Offer valid type and data.
ofr := &offer{data: "hello"}
transfer.OfferOp{
Tag: src,
Type: "file",
Data: ofr,
}.Add(ops)
r.Frame(ops)
assertEventPointerTypeSequence(t, r.Events(tag), pointer.Leave)
})
} }
func TestDeferredInputOp(t *testing.T) { func TestDeferredInputOp(t *testing.T) {
+2 -8
View File
@@ -5,7 +5,6 @@ package input
import ( import (
"encoding/binary" "encoding/binary"
"image" "image"
"io"
"strings" "strings"
"time" "time"
@@ -221,6 +220,8 @@ func (q *Router) executeCommands() {
q.key.queue.softKeyboard(req.Show) q.key.queue.softKeyboard(req.Show)
case key.SnippetCmd: case key.SnippetCmd:
q.key.queue.setSnippet(req) q.key.queue.setSnippet(req)
case transfer.OfferCmd:
q.pointer.queue.offerData(req, &q.handlers)
} }
} }
q.commands = nil q.commands = nil
@@ -498,13 +499,6 @@ func (q *Router) collect() {
Type: encOp.Refs[1].(string), Type: encOp.Refs[1].(string),
} }
pc.targetOp(op, &q.handlers) pc.targetOp(op, &q.handlers)
case ops.TypeOffer:
op := transfer.OfferOp{
Tag: encOp.Refs[0].(event.Tag),
Type: encOp.Refs[1].(string),
Data: encOp.Refs[2].(io.ReadCloser),
}
pc.offerOp(op, &q.handlers)
case ops.TypeActionInput: case ops.TypeActionInput:
act := system.Action(encOp.Data[1]) act := system.Action(encOp.Data[1])
pc.actionInputOp(act) pc.actionInputOp(act)
+21 -29
View File
@@ -2,11 +2,11 @@
// //
// The transfer protocol is as follows: // The transfer protocol is as follows:
// //
// - Data sources are registered with SourceOps, data targets with TargetOps. // - Data sources use [SourceFilter] to receive [InitiateEvent]s when a drag
// - A data source receives a RequestEvent when a transfer is initiated. // is initiated, and [RequestEvent]s for each initiation of a data transfer.
// It must respond with an OfferOp. // Sources respond to requests with [OfferCommand].
// - The target receives a DataEvent when transferring to it. It must close // - Data targets use [TargetFilter] to receive [DataEvent]s for receiving data.
// the event data after use. // The target must close the data event after use.
// //
// When a user initiates a pointer-guided drag and drop transfer, the // When a user initiates a pointer-guided drag and drop transfer, the
// source as well as all potential targets receive an InitiateEvent. // source as well as all potential targets receive an InitiateEvent.
@@ -25,6 +25,21 @@ import (
"gioui.org/op" "gioui.org/op"
) )
// OfferCmd is used by data sources as a response to a RequestEvent.
type OfferCmd struct {
Tag event.Tag
// Type is the MIME type of Data.
// It must be the Type from the corresponding RequestEvent.
Type string
// Data contains the offered data. It is closed when the
// transfer is complete or cancelled.
// Data must be kept valid until closed, and it may be used from
// a goroutine separate from the one processing the frame..
Data io.ReadCloser
}
func (OfferCmd) ImplementsCommand() {}
// SourceOp registers a tag as a data source for a MIME type. // SourceOp registers a tag as a data source for a MIME type.
// Use multiple SourceOps if a tag supports multiple types. // Use multiple SourceOps if a tag supports multiple types.
type SourceOp struct { type SourceOp struct {
@@ -41,19 +56,6 @@ type TargetOp struct {
Type string Type string
} }
// OfferOp is used by data sources as a response to a RequestEvent.
type OfferOp struct {
Tag event.Tag
// Type is the MIME type of Data.
// It must be the Type from the corresponding RequestEvent.
Type string
// Data contains the offered data. It is closed when the
// transfer is complete or cancelled.
// Data must be kept valid until closed, and it may be used from
// a goroutine separate from the one processing the frame..
Data io.ReadCloser
}
func (op SourceOp) Add(o *op.Ops) { func (op SourceOp) Add(o *op.Ops) {
data := ops.Write2(&o.Internal, ops.TypeSourceLen, op.Tag, op.Type) data := ops.Write2(&o.Internal, ops.TypeSourceLen, op.Tag, op.Type)
data[0] = byte(ops.TypeSource) data[0] = byte(ops.TypeSource)
@@ -64,18 +66,8 @@ func (op TargetOp) Add(o *op.Ops) {
data[0] = byte(ops.TypeTarget) data[0] = byte(ops.TypeTarget)
} }
// Add the offer to the list of operations.
// It panics if the Data field is not set.
func (op OfferOp) Add(o *op.Ops) {
if op.Data == nil {
panic("invalid nil data in OfferOp")
}
data := ops.Write3(&o.Internal, ops.TypeOfferLen, op.Tag, op.Type, op.Data)
data[0] = byte(ops.TypeOffer)
}
// RequestEvent requests data from a data source. The source must // RequestEvent requests data from a data source. The source must
// respond with an OfferOp. // respond with an OfferCmd.
type RequestEvent struct { type RequestEvent struct {
// Type is the first matched type between the source and the target. // Type is the first matched type between the source and the target.
Type string Type string
+2 -6
View File
@@ -77,12 +77,8 @@ func (d *Draggable) Update(gtx layout.Context) (mime string, requested bool) {
// Offer the data ready for a drop. Must be called after being Requested. // Offer the data ready for a drop. Must be called after being Requested.
// The mime must be one in the requested list. // The mime must be one in the requested list.
func (d *Draggable) Offer(ops *op.Ops, mime string, data io.ReadCloser) { func (d *Draggable) Offer(gtx layout.Context, mime string, data io.ReadCloser) {
transfer.OfferOp{ gtx.Queue(transfer.OfferCmd{Tag: &d.handle, Type: mime, Data: data})
Tag: &d.handle,
Type: mime,
Data: data,
}.Add(ops)
} }
// Pos returns the drag position relative to its initial click position. // Pos returns the drag position relative to its initial click position.
+3 -3
View File
@@ -51,12 +51,12 @@ func TestDraggable(t *testing.T) {
}, },
) )
ofr := &offer{data: "hello"} ofr := &offer{data: "hello"}
drag.Offer(gtx.Ops, "file", ofr) drag.Offer(gtx, "file", ofr)
r.Frame(gtx.Ops) r.Frame(gtx.Ops)
evs := r.Events(drag) evs := r.Events(drag)
if len(evs) != 1 { if len(evs) != 2 {
t.Fatalf("expected 1 event, got %d", len(evs)) t.Fatalf("expected 2 event, got %d", len(evs))
} }
ev := evs[0].(transfer.DataEvent) ev := evs[0].(transfer.DataEvent)
ev.Open = nil ev.Open = nil
+1 -1
View File
@@ -94,7 +94,7 @@ func ExampleDraggable_Layout() {
// drag must respond with an Offer event when requested. // drag must respond with an Offer event when requested.
// Use the drag method for this. // Use the drag method for this.
if m, ok := drag.Update(gtx); ok { if m, ok := drag.Update(gtx); ok {
drag.Offer(gtx.Ops, m, offer{Data: "hello world"}) drag.Offer(gtx, m, offer{Data: "hello world"})
} }
// Setup the area for drops. // Setup the area for drops.