Skip to content

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 toSource implementsTarget implements
move something within your appevent.Draggable plus core.DragPayloadProvider and/or core.DragImageProvidercore.DragTarget
drag text, files or an image out to another appevent.Draggable, calling desktop.StartDragthe other app
accept files dragged in from the desktopthe file managerbellui.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:

MethodWhen
DragEnterthe pointer moves onto the target
DragOverthe pointer moves while over it
DragLeavethe pointer moves off it, or the drag ends elsewhere
Dropthe 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. Only file:// 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, when Image is 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.

PlatformDrag out
macOSyes
Windowsyes
Linux, X11yes
Linux, Waylandyes, but OnCompleted is never called and no drag picture is shown
BSDs (X11)yes
Webyes, through DragOutProvider (below)
iOS, Androidno; 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.

One type cannot be both a 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)
}