Drag and drop
Bellui supports three kinds of drag and drop. Each one is a set of small interfaces that your widget implements:
| You want to | Source implements | Target implements |
|---|---|---|
| move something within your app | event.Draggable plus core.DragPayloadProvider and/or core.DragImageProvider | core.DragTarget |
| drag text, files or an image out to another app | event.Draggable, calling desktop.StartDrag | the other app |
| accept files dragged in from the desktop | the file manager | bellui.Droppable, optionally bellui.DropHoverable |
The example at the end of the page shows all three in one window.
Within your app
A drag source is a widget that implements
event.Draggable.
To take part in drag and drop, it also implements one or both of these:
core.DragPayloadProvider:DragPayload()returns the value the target receives, of any type.core.DragImageProvider:DragImage()returns an object that Bellui draws under the pointer during the drag (a “ghost”). Return nil for none.
Both are read once, when the drag starts.
// card is a drag source for in-app drag and drop.
type card struct {
widget.BaseWidget
name string
}
func newCard(name string) *card {
c := &card{name: name}
c.ExtendBaseWidget(c)
return c
}
func (c *card) CreateRenderer() core.WidgetRenderer {
return widget.NewSimpleRenderer(container.NewStack(canvas.NewRectangle(hover), widget.NewLabel(c.name)))
}
// event.Draggable: the driver calls these as the pointer moves and on release.
func (c *card) Dragged(*event.DragEvent) {}
func (c *card) DragEnd() {}
// core.DragPayloadProvider: the value handed to the drop target.
func (c *card) DragPayload() any { return c.name }
// core.DragImageProvider: the "ghost" painted under the pointer.
func (c *card) DragImage() core.CanvasObject { return widget.NewLabel(c.name) }A drop target implements
core.DragTarget.
Bellui finds the deepest target under the pointer and calls it as the drag
moves:
| Method | When |
|---|---|
DragEnter | the pointer moves onto the target |
DragOver | the pointer moves while over it |
DragLeave | the pointer moves off it, or the drag ends elsewhere |
Drop | the user releases over it |
Each call except DragLeave gets a
DragTargetEvent
with the payload and the pointer position. Position is relative to the
target, and AbsolutePosition to the window. Use DragEnter and DragLeave to highlight the target:
// bin is an in-app drop target that highlights while a card is over it.
type bin struct {
widget.BaseWidget
bg *canvas.Rectangle
label *widget.Label
onDrop func(name string)
}
func newBin(onDrop func(string)) *bin {
b := &bin{bg: canvas.NewRectangle(idle), label: widget.NewLabel("Drop a card here"), onDrop: onDrop}
b.ExtendBaseWidget(b)
return b
}
func (b *bin) CreateRenderer() core.WidgetRenderer {
return widget.NewSimpleRenderer(container.NewStack(b.bg, container.NewCenter(b.label)))
}
func (b *bin) setColor(c color.Color) { b.bg.FillColor = c; b.bg.Refresh() }
// core.DragTarget
func (b *bin) DragEnter(*core.DragTargetEvent) { b.setColor(hover) }
func (b *bin) DragOver(*core.DragTargetEvent) {}
func (b *bin) DragLeave() { b.setColor(idle) }
func (b *bin) Drop(e *core.DragTargetEvent) {
b.setColor(idle)
if name, ok := e.Payload.(string); ok {
b.onDrop(name)
}
}Only drags whose source provides a payload or an image reach drop targets. A
slider or a scroll bar is also Draggable, and dragging it never triggers
your targets.
Out to other apps
desktop.StartDrag
hands the current drag to the operating system, so the user can drop into a
file manager, a text editor or a browser. Call it from your widget’s Dragged
method:
// export drags a piece of text out of the app, into an editor or browser.
type export struct {
widget.BaseWidget
text string
started bool
}
func newExport(text string) *export {
e := &export{text: text}
e.ExtendBaseWidget(e)
return e
}
func (e *export) CreateRenderer() core.WidgetRenderer {
return widget.NewSimpleRenderer(widget.NewLabel("Drag me to another app"))
}
func (e *export) data() desktop.DragData {
return desktop.DragData{
Text: e.text,
OnCompleted: func(accepted bool) {
fmt.Println("drop accepted:", accepted)
},
}
}
// Desktop: start the native drag from the first Dragged call of a gesture.
func (e *export) Dragged(*event.DragEvent) {
if !e.started {
e.started = desktop.StartDrag(e.data())
}
}
func (e *export) DragEnd() { e.started = false }
// Web: the browser asks for the payload when the gesture starts.
func (e *export) DragOutData() (desktop.DragData, bool) { return e.data(), true }DragData
holds what is dragged. Set any of:
Text: plain text.URIs: files. Onlyfile://URIs are carried; others are skipped.Image: an image. It is also the picture that follows the pointer.DragImageObject: a Bellui object to draw as that picture instead, whenImageis nil. It is only a picture and is never dropped as data.OnCompleted: called on the UI thread when the drag ends, with whether a target accepted the drop. A move can delete the original here.
StartDrag returns false when the platform cannot start a drag. Your widget
then keeps its in-app behavior. Once it returns true, the operating system owns
the gesture and Bellui ends its own drag, calling DragEnd.
| Platform | Drag out |
|---|---|
| macOS | yes |
| Windows | yes |
| Linux, X11 | yes |
| Linux, Wayland | yes, but OnCompleted is never called and no drag picture is shown |
| BSDs (X11) | yes |
| Web | yes, through DragOutProvider (below) |
| iOS, Android | no; StartDrag returns false |
On the web
In a browser, Dragged is not called once the browser’s own drag has begun,
so StartDrag from Dragged cannot work there. Implement
desktop.DragOutProvider
as well. Its DragOutData() returns the data and true, or false to keep an
ordinary in-app drag. The web build asks for it when the pointer goes down on
your widget. The example’s export widget implements both, so it works on the
desktop and on the web.
Files from the desktop
A widget that implements
bellui.Droppable
receives files the user drops on it:
// filesIn accepts files dropped from the desktop's file manager.
type filesIn struct {
widget.BaseWidget
label *widget.Label
}
func newFilesIn() *filesIn {
f := &filesIn{label: widget.NewLabel("Drop files from your desktop here")}
f.ExtendBaseWidget(f)
return f
}
func (f *filesIn) CreateRenderer() core.WidgetRenderer {
return widget.NewSimpleRenderer(f.label)
}
// bellui.Droppable
func (f *filesIn) Dropped(e *bellui.DropEvent) {
f.label.SetText(fmt.Sprintf("Received %d item(s), first: %s", len(e.URIs), e.URIs[0].Name()))
}The DropEvent
has the drop position, relative to the widget and to the window, and the items
as URIs. On the desktop these are file URIs.
Bellui gives the drop to the deepest Droppable under the pointer. A drop that
no widget claims goes to the window’s callback, if you set one:
// Drops that no Droppable claims fall back to the window-wide callback.
if dw, ok := w.(bellui.DroppableWindow); ok {
dw.SetOnDropped(func(pos geo.Position, items []bellui.URI) {
status.SetText(fmt.Sprintf("%d item(s) dropped on the window", len(items)))
})
}To highlight a widget while files are dragged over it, implement
bellui.DropHoverable
too. It adds DragEnter, DragOver and DragLeave, each with only the
pointer position: the operating system does not reveal the files until the
drop. These hover calls arrive on macOS, Windows and X11. Elsewhere the widget
still gets Dropped, just without the hover calls.
bellui.DropHoverable and a core.DragTarget.
Both interfaces have methods named DragEnter and DragOver, with different
argument types. Use two widgets, for example one wrapped in the other, when an
area should accept both files and in-app drags.The whole example
// Drag and drop: inside the app, out to other apps, and files in from the OS.
package main
import (
"fmt"
"image/color"
"github.com/belluikit/bellui"
"github.com/belluikit/bellui/app"
"github.com/belluikit/bellui/canvas"
"github.com/belluikit/bellui/container"
"github.com/belluikit/bellui/core"
"github.com/belluikit/bellui/driver/desktop"
"github.com/belluikit/bellui/event"
"github.com/belluikit/bellui/geo"
"github.com/belluikit/bellui/widget"
)
var (
idle = color.NRGBA{R: 0x9E, G: 0x9E, B: 0x9E, A: 0x40}
hover = color.NRGBA{R: 0x1E, G: 0x88, B: 0xE5, A: 0x60}
)
// card is a drag source for in-app drag and drop.
type card struct {
widget.BaseWidget
name string
}
func newCard(name string) *card {
c := &card{name: name}
c.ExtendBaseWidget(c)
return c
}
func (c *card) CreateRenderer() core.WidgetRenderer {
return widget.NewSimpleRenderer(container.NewStack(canvas.NewRectangle(hover), widget.NewLabel(c.name)))
}
// event.Draggable: the driver calls these as the pointer moves and on release.
func (c *card) Dragged(*event.DragEvent) {}
func (c *card) DragEnd() {}
// core.DragPayloadProvider: the value handed to the drop target.
func (c *card) DragPayload() any { return c.name }
// core.DragImageProvider: the "ghost" painted under the pointer.
func (c *card) DragImage() core.CanvasObject { return widget.NewLabel(c.name) }
// bin is an in-app drop target that highlights while a card is over it.
type bin struct {
widget.BaseWidget
bg *canvas.Rectangle
label *widget.Label
onDrop func(name string)
}
func newBin(onDrop func(string)) *bin {
b := &bin{bg: canvas.NewRectangle(idle), label: widget.NewLabel("Drop a card here"), onDrop: onDrop}
b.ExtendBaseWidget(b)
return b
}
func (b *bin) CreateRenderer() core.WidgetRenderer {
return widget.NewSimpleRenderer(container.NewStack(b.bg, container.NewCenter(b.label)))
}
func (b *bin) setColor(c color.Color) { b.bg.FillColor = c; b.bg.Refresh() }
// core.DragTarget
func (b *bin) DragEnter(*core.DragTargetEvent) { b.setColor(hover) }
func (b *bin) DragOver(*core.DragTargetEvent) {}
func (b *bin) DragLeave() { b.setColor(idle) }
func (b *bin) Drop(e *core.DragTargetEvent) {
b.setColor(idle)
if name, ok := e.Payload.(string); ok {
b.onDrop(name)
}
}
// export drags a piece of text out of the app, into an editor or browser.
type export struct {
widget.BaseWidget
text string
started bool
}
func newExport(text string) *export {
e := &export{text: text}
e.ExtendBaseWidget(e)
return e
}
func (e *export) CreateRenderer() core.WidgetRenderer {
return widget.NewSimpleRenderer(widget.NewLabel("Drag me to another app"))
}
func (e *export) data() desktop.DragData {
return desktop.DragData{
Text: e.text,
OnCompleted: func(accepted bool) {
fmt.Println("drop accepted:", accepted)
},
}
}
// Desktop: start the native drag from the first Dragged call of a gesture.
func (e *export) Dragged(*event.DragEvent) {
if !e.started {
e.started = desktop.StartDrag(e.data())
}
}
func (e *export) DragEnd() { e.started = false }
// Web: the browser asks for the payload when the gesture starts.
func (e *export) DragOutData() (desktop.DragData, bool) { return e.data(), true }
// filesIn accepts files dropped from the desktop's file manager.
type filesIn struct {
widget.BaseWidget
label *widget.Label
}
func newFilesIn() *filesIn {
f := &filesIn{label: widget.NewLabel("Drop files from your desktop here")}
f.ExtendBaseWidget(f)
return f
}
func (f *filesIn) CreateRenderer() core.WidgetRenderer {
return widget.NewSimpleRenderer(f.label)
}
// bellui.Droppable
func (f *filesIn) Dropped(e *bellui.DropEvent) {
f.label.SetText(fmt.Sprintf("Received %d item(s), first: %s", len(e.URIs), e.URIs[0].Name()))
}
func main() {
a := app.New()
w := a.NewWindow("Drag and drop")
status := widget.NewLabel("")
target := newBin(func(name string) { status.SetText("Dropped " + name) })
// Drops that no Droppable claims fall back to the window-wide callback.
if dw, ok := w.(bellui.DroppableWindow); ok {
dw.SetOnDropped(func(pos geo.Position, items []bellui.URI) {
status.SetText(fmt.Sprintf("%d item(s) dropped on the window", len(items)))
})
}
w.SetContent(container.NewPadded(container.NewVBox(
container.NewHBox(newCard("Alpha"), newCard("Beta")),
container.NewGridWrap(geo.NewSize(300, 80), target),
newExport("Hello from Bellui"),
newFilesIn(),
status,
)))
w.Resize(geo.NewSize(420, 360))
bellui.ShowAndRun(w)
}