diff --git a/internal/ops/ops.go b/internal/ops/ops.go index bf87e5fa..23c23580 100644 --- a/internal/ops/ops.go +++ b/internal/ops/ops.go @@ -67,7 +67,6 @@ const ( TypeClipboardWrite TypeSource TypeTarget - TypeOffer TypeKeyInput TypeSave TypeLoad @@ -148,7 +147,6 @@ const ( TypeClipboardWriteLen = 1 TypeSourceLen = 1 TypeTargetLen = 1 - TypeOfferLen = 1 TypeKeyInputLen = 1 + 1 TypeSaveLen = 1 + 4 TypeLoadLen = 1 + 4 @@ -427,7 +425,6 @@ var opProps = [0x100]opProp{ TypeClipboardWrite: {Size: TypeClipboardWriteLen, NumRefs: 1}, TypeSource: {Size: TypeSourceLen, NumRefs: 2}, TypeTarget: {Size: TypeTargetLen, NumRefs: 2}, - TypeOffer: {Size: TypeOfferLen, NumRefs: 3}, TypeKeyInput: {Size: TypeKeyInputLen, NumRefs: 2}, TypeSave: {Size: TypeSaveLen, NumRefs: 0}, TypeLoad: {Size: TypeLoadLen, NumRefs: 0}, @@ -498,8 +495,6 @@ func (t OpType) String() string { return "Source" case TypeTarget: return "Target" - case TypeOffer: - return "Offer" case TypeKeyInput: return "KeyInput" case TypeSave: diff --git a/io/input/pointer.go b/io/input/pointer.go index 1e87d823..770f938f 100644 --- a/io/input/pointer.go +++ b/io/input/pointer.go @@ -72,8 +72,6 @@ type pointerHandler struct { sourceMimes []string targetMimes []string - offeredMime string - data io.ReadCloser } type areaOp struct { @@ -236,16 +234,23 @@ func (c *pointerCollector) newHandler(tag event.Tag, events *handlerEvents) *poi tag: tag, 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 { - h = new(pointerHandler) - c.q.handlers[tag] = h + h = &pointerHandler{ + area: -1, + } + q.handlers[tag] = h // Cancel handlers on (each) first appearance, but don't // trigger redraw. events.AddNoRedraw(tag, pointer.Event{Kind: pointer.Cancel}) } h.active = true - h.area = areaID return h } @@ -332,10 +337,27 @@ func (c *pointerCollector) targetOp(op transfer.TargetOp, events *handlerEvents) h.targetMimes = append(h.targetMimes, op.Type) } -func (c *pointerCollector) offerOp(op transfer.OfferOp, events *handlerEvents) { - h := c.newHandler(op.Tag, events) - h.offeredMime = op.Type - h.data = op.Data +func (q *pointerQueue) offerData(req transfer.OfferCmd, events *handlerEvents) { + transferIdx := len(q.transfers) + q.transfers = append(q.transfers, req.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() { @@ -537,6 +559,7 @@ func (q *pointerQueue) reset() { for _, h := range q.handlers { // Reset handler. h.active = false + h.area = -1 h.wantsGrab = false h.types = 0 h.sourceMimes = h.sourceMimes[:0] @@ -596,7 +619,6 @@ func (q *pointerQueue) Frame(events *handlerEvents) { for i := range q.pointers { p := &q.pointers[i] q.deliverEnterLeaveEvents(p, events, p.last) - q.deliverTransferDataEvent(p, events) } } @@ -790,10 +812,10 @@ func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEv continue } h := q.handlers[k] + e := e e.Kind = pointer.Leave if e.Kind&h.types != 0 { - e := e e.Position = q.invTransform(h.area, e.Position) events.Add(k, e) } @@ -804,10 +826,10 @@ func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEv if _, found := searchTag(p.entered, k); found { continue } + e := e e.Kind = pointer.Enter if e.Kind&h.types != 0 { - e := e e.Position = q.invTransform(h.area, e.Position) events.Add(k, e) } @@ -855,32 +877,6 @@ func (q *pointerQueue) deliverDropEvent(p *pointerInfo, events *handlerEvents) { 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) { events.Add(p.dataSource, transfer.CancelEvent{}) // Cancel all potential targets. @@ -890,8 +886,6 @@ func (q *pointerQueue) deliverTransferCancelEvent(p *pointerInfo, events *handle events.Add(k, transfer.CancelEvent{}) } } - src.offeredMime = "" - src.data = nil p.dataSource = nil p.dataTarget = nil } diff --git a/io/input/pointer_test.go b/io/input/pointer_test.go index 18c0cb97..8f6ff6c3 100644 --- a/io/input/pointer_test.go +++ b/io/input/pointer_test.go @@ -821,15 +821,6 @@ func TestTransfer(t *testing.T) { // Cancel is received when the pointer is first seen. 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) { ops := new(op.Ops) src, tgt := setup(ops, "file", "file") @@ -959,16 +950,13 @@ func TestTransfer(t *testing.T) { // Offer valid type and data. ofr := &offer{data: "hello"} - transfer.OfferOp{ - Tag: src, - Type: "file", - Data: ofr, - }.Add(ops) + r.Source().Queue(transfer.OfferCmd{Tag: src, Type: "file", Data: ofr}) r.Frame(ops) evs := r.Events(tgt) - if len(evs) != 1 { - t.Fatalf("unexpected number of events: %d, want 1", len(evs)) + if len(evs) != 2 { + t.Fatalf("unexpected number of events: %d, want 2", len(evs)) } + assertEventSequence(t, evs[1:], transfer.CancelEvent{}) dataEvent, ok := evs[0].(transfer.DataEvent) if !ok { t.Fatalf("unexpected event type: %T, want %T", dataEvent, transfer.DataEvent{}) @@ -984,9 +972,8 @@ func TestTransfer(t *testing.T) { if ofr.closed { t.Error("offer closed prematurely") } - r.Frame(ops) 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) { @@ -1017,122 +1004,16 @@ func TestTransfer(t *testing.T) { }, ) ofr := &offer{data: "hello"} - transfer.OfferOp{ - 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.Source().Queue(transfer.OfferCmd{Tag: src, Type: "file", Data: ofr}) r.Frame(ops) 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 { 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) { diff --git a/io/input/router.go b/io/input/router.go index 103a1c35..291f7d9e 100644 --- a/io/input/router.go +++ b/io/input/router.go @@ -5,7 +5,6 @@ package input import ( "encoding/binary" "image" - "io" "strings" "time" @@ -221,6 +220,8 @@ func (q *Router) executeCommands() { q.key.queue.softKeyboard(req.Show) case key.SnippetCmd: q.key.queue.setSnippet(req) + case transfer.OfferCmd: + q.pointer.queue.offerData(req, &q.handlers) } } q.commands = nil @@ -498,13 +499,6 @@ func (q *Router) collect() { Type: encOp.Refs[1].(string), } 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: act := system.Action(encOp.Data[1]) pc.actionInputOp(act) diff --git a/io/transfer/transfer.go b/io/transfer/transfer.go index 78e2b900..1862c24e 100644 --- a/io/transfer/transfer.go +++ b/io/transfer/transfer.go @@ -2,11 +2,11 @@ // // The transfer protocol is as follows: // -// - Data sources are registered with SourceOps, data targets with TargetOps. -// - A data source receives a RequestEvent when a transfer is initiated. -// It must respond with an OfferOp. -// - The target receives a DataEvent when transferring to it. It must close -// the event data after use. +// - Data sources use [SourceFilter] to receive [InitiateEvent]s when a drag +// is initiated, and [RequestEvent]s for each initiation of a data transfer. +// Sources respond to requests with [OfferCommand]. +// - Data targets use [TargetFilter] to receive [DataEvent]s for receiving data. +// The target must close the data event after use. // // When a user initiates a pointer-guided drag and drop transfer, the // source as well as all potential targets receive an InitiateEvent. @@ -25,6 +25,21 @@ import ( "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. // Use multiple SourceOps if a tag supports multiple types. type SourceOp struct { @@ -41,19 +56,6 @@ type TargetOp struct { 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) { data := ops.Write2(&o.Internal, ops.TypeSourceLen, op.Tag, op.Type) data[0] = byte(ops.TypeSource) @@ -64,18 +66,8 @@ func (op TargetOp) Add(o *op.Ops) { 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 -// respond with an OfferOp. +// respond with an OfferCmd. type RequestEvent struct { // Type is the first matched type between the source and the target. Type string diff --git a/widget/dnd.go b/widget/dnd.go index 48fc559b..6ced658e 100644 --- a/widget/dnd.go +++ b/widget/dnd.go @@ -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. // The mime must be one in the requested list. -func (d *Draggable) Offer(ops *op.Ops, mime string, data io.ReadCloser) { - transfer.OfferOp{ - Tag: &d.handle, - Type: mime, - Data: data, - }.Add(ops) +func (d *Draggable) Offer(gtx layout.Context, mime string, data io.ReadCloser) { + gtx.Queue(transfer.OfferCmd{Tag: &d.handle, Type: mime, Data: data}) } // Pos returns the drag position relative to its initial click position. diff --git a/widget/dnd_test.go b/widget/dnd_test.go index 3c106376..998eec4c 100644 --- a/widget/dnd_test.go +++ b/widget/dnd_test.go @@ -51,12 +51,12 @@ func TestDraggable(t *testing.T) { }, ) ofr := &offer{data: "hello"} - drag.Offer(gtx.Ops, "file", ofr) + drag.Offer(gtx, "file", ofr) r.Frame(gtx.Ops) evs := r.Events(drag) - if len(evs) != 1 { - t.Fatalf("expected 1 event, got %d", len(evs)) + if len(evs) != 2 { + t.Fatalf("expected 2 event, got %d", len(evs)) } ev := evs[0].(transfer.DataEvent) ev.Open = nil diff --git a/widget/example_test.go b/widget/example_test.go index 940ede19..e4edb3aa 100644 --- a/widget/example_test.go +++ b/widget/example_test.go @@ -94,7 +94,7 @@ func ExampleDraggable_Layout() { // drag must respond with an Offer event when requested. // Use the drag method for this. 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.