Skip to content

Input events

Bellui delivers input to an object by checking which interfaces it implements. There is no event registration: if the object under the pointer has a Tapped method with the right signature, a click calls it. The interfaces are in the event package.

Ready-made widgets already implement the interfaces they need; a button is Tappable, an entry is Focusable. You implement them yourself when you build a widget. The canvas primitives implement none of them, by design.

A widget that receives everything

This widget implements every pointer interface except DoubleTappable (see the warning below) and reports what it gets:

// pad is a widget that implements several event interfaces. The toolkit
// checks which interfaces an object has and delivers matching events.
type pad struct {
	widget.BaseWidget
	bg  *canvas.Rectangle
	log func(string)
}

func newPad(log func(string)) *pad {
	p := &pad{bg: canvas.NewRectangle(color.NRGBA{R: 0x3b, G: 0x6e, B: 0xc8, A: 0xff}), log: log}
	p.ExtendBaseWidget(p)
	return p
}

func (p *pad) CreateRenderer() core.WidgetRenderer {
	return widget.NewSimpleRenderer(p.bg)
}

// event.Tappable: a click or a tap.
func (p *pad) Tapped(e *event.PointEvent) { p.log(fmt.Sprintf("Tapped at %v", e.Position)) }

// event.SecondaryTappable: a right-click on desktop.
func (p *pad) TappedSecondary(*event.PointEvent) { p.log("TappedSecondary") }

// event.LongPressable: press and hold.
func (p *pad) LongPressed(*event.PointEvent) { p.log("LongPressed") }

// event.Hoverable: the pointer enters, moves over and leaves (desktop and web).
func (p *pad) HoverIn(*event.HoverEvent)    { p.log("HoverIn") }
func (p *pad) HoverMoved(*event.HoverEvent) {}
func (p *pad) HoverOut()                    { p.log("HoverOut") }

// event.Draggable: the pointer moves while pressed.
func (p *pad) Dragged(e *event.DragEvent) { p.log(fmt.Sprintf("Dragged by %v", e.Dragged)) }
func (p *pad) DragEnd()                   { p.log("DragEnd") }

// event.Scrollable: a mouse wheel or touchpad scroll.
func (p *pad) Scrolled(e *event.ScrollEvent) { p.log(fmt.Sprintf("Scrolled %v", e.Scrolled)) }

widget.BaseWidget, ExtendBaseWidget and CreateRenderer are explained in Custom widgets. Every event method runs on the main goroutine.

Pointer interfaces

InterfaceMethodsDelivered when
TappableTapped(*PointEvent)A click or tap is pressed and released on the object
DoubleTappableDoubleTapped(*PointEvent)Two taps in quick succession
SecondaryTappableTappedSecondary(*PointEvent)A right-click on desktop; on touch, see below
LongPressableLongPressed(*PointEvent)The pointer is held down without moving
HoverableHoverIn, HoverMoved, HoverOutA mouse pointer enters, moves over, or leaves the object
DraggableDragged(*DragEvent), DragEnd()The pointer moves while pressed, and then is released
ScrollableScrolled(*ScrollEvent)A mouse wheel or touchpad scrolls over the object

A PointEvent has Position, relative to the object’s top-left corner, and AbsolutePosition, relative to the window. DragEvent.Dragged and ScrollEvent.Scrolled are a geo.Delta: how far the pointer moved since the last event. ScrollEvent.Source says whether a scroll came from a mouse wheel, a touchpad or a touch gesture.

If an object implements DoubleTappable, a single tap is not delivered straight away. The toolkit waits to see whether a second tap follows, and calls Tapped only after that delay has passed. Implement DoubleTappable only when you need it.

Desktop and touch

The same interfaces work on every platform, but some gestures map differently:

GestureDesktop and webTouch
TapPrimary button clickFinger tap
Secondary tapRight-clickLifting the finger after a press and hold
Long pressHolding the primary button down; it fires while the button is still down, and the release does not also deliver TappedHolding a finger still; it fires while the finger is still down
HoverMouse movementNot delivered: there is no pointer between touches
ScrollMouse wheel or touchpadDragging a finger over scrollable content, such as a scroll container

On touch, a widget that implements both LongPressable and SecondaryTappable gets LongPressed while the finger is held, and then TappedSecondary when it lifts.

For desktop-only details there are extra interfaces in driver/desktop: Mouseable (MouseDown and MouseUp, with the button and modifier keys), Keyable (KeyDown and KeyUp) and Cursorable (the mouse cursor to show over the object). The driver/mobile package has Touchable (TouchDown, TouchUp, TouchCancel) for raw touch events.

Keyboard input

Key events go to the focused object. An object can take focus if it implements Focusable:

MethodCalled when
FocusGained(), FocusLost()The object gains or loses focus
TypedRune(rune)A character is typed
TypedKey(*KeyEvent)A key without a character is pressed, such as an arrow key or Return

KeyEvent.Name is a KeyName such as event.KeyReturn or event.KeyEscape. KeyEvent.Modifier holds the modifier keys that were down. On desktop, Tab moves focus to the next focusable object. Your code can move focus with w.Canvas().Focus(obj).

On desktop, a key pressed with Control, Alt or Super (Command on macOS) goes to the shortcut system, not to TypedKey. Only unmodified and Shift-modified keys reach TypedKey.

When no object has focus, key events go to the window’s canvas instead. Handle them there with SetOnTypedKey and SetOnTypedRune, as the example below does.

Shortcuts

A shortcut is a key combination with a name. The bellui package defines the standard ones (ShortcutCopy, ShortcutPaste, ShortcutCut, ShortcutSelectAll, ShortcutUndo, ShortcutRedo and more). For any other combination, use desktop.CustomShortcut. Add a window-wide shortcut on the window’s canvas:

// A shortcut for the whole window: Ctrl+S, or Cmd+S on macOS.
save := &desktop.CustomShortcut{KeyName: event.KeyS, Modifier: event.KeyModifierShortcutDefault}
w.Canvas().AddShortcut(save, func(core.Shortcut) {
	show("Save shortcut")
})

// Keys that no focused widget consumed.
w.Canvas().SetOnTypedKey(func(e *event.KeyEvent) {
	show(fmt.Sprintf("Key %s", e.Name))
})

event.KeyModifierShortcutDefault is Command in an app built for macOS or iOS, and Control in every other build, so one shortcut follows each platform’s convention. It is fixed when you compile: a web build uses Control, even in a browser on a Mac.

When a shortcut is pressed, the toolkit tries, in order:

  1. a matching shortcut in the window’s main menu;
  2. the focused object, if it implements bellui.Shortcutable (TypedShortcut);
  3. the shortcuts added to the canvas with AddShortcut.

So a focused text entry receives Ctrl+C before a canvas shortcut for the same keys would. To give your own widget shortcuts, embed a bellui.ShortcutHandler, which provides TypedShortcut, AddShortcut and RemoveShortcut.

The complete example:

// A pad that reports every pointer event it receives, plus a window-wide
// keyboard shortcut and key handler.
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/widget"
)

// pad is a widget that implements several event interfaces. The toolkit
// checks which interfaces an object has and delivers matching events.
type pad struct {
	widget.BaseWidget
	bg  *canvas.Rectangle
	log func(string)
}

func newPad(log func(string)) *pad {
	p := &pad{bg: canvas.NewRectangle(color.NRGBA{R: 0x3b, G: 0x6e, B: 0xc8, A: 0xff}), log: log}
	p.ExtendBaseWidget(p)
	return p
}

func (p *pad) CreateRenderer() core.WidgetRenderer {
	return widget.NewSimpleRenderer(p.bg)
}

// event.Tappable: a click or a tap.
func (p *pad) Tapped(e *event.PointEvent) { p.log(fmt.Sprintf("Tapped at %v", e.Position)) }

// event.SecondaryTappable: a right-click on desktop.
func (p *pad) TappedSecondary(*event.PointEvent) { p.log("TappedSecondary") }

// event.LongPressable: press and hold.
func (p *pad) LongPressed(*event.PointEvent) { p.log("LongPressed") }

// event.Hoverable: the pointer enters, moves over and leaves (desktop and web).
func (p *pad) HoverIn(*event.HoverEvent)    { p.log("HoverIn") }
func (p *pad) HoverMoved(*event.HoverEvent) {}
func (p *pad) HoverOut()                    { p.log("HoverOut") }

// event.Draggable: the pointer moves while pressed.
func (p *pad) Dragged(e *event.DragEvent) { p.log(fmt.Sprintf("Dragged by %v", e.Dragged)) }
func (p *pad) DragEnd()                   { p.log("DragEnd") }

// event.Scrollable: a mouse wheel or touchpad scroll.
func (p *pad) Scrolled(e *event.ScrollEvent) { p.log(fmt.Sprintf("Scrolled %v", e.Scrolled)) }


func main() {
	a := app.NewWithID("com.example.events")
	w := a.NewWindow("Input events")

	last := widget.NewLabel("Interact with the blue pad")
	show := func(s string) { last.SetText(s) }

	// A shortcut for the whole window: Ctrl+S, or Cmd+S on macOS.
	save := &desktop.CustomShortcut{KeyName: event.KeyS, Modifier: event.KeyModifierShortcutDefault}
	w.Canvas().AddShortcut(save, func(core.Shortcut) {
		show("Save shortcut")
	})

	// Keys that no focused widget consumed.
	w.Canvas().SetOnTypedKey(func(e *event.KeyEvent) {
		show(fmt.Sprintf("Key %s", e.Name))
	})

	w.SetContent(container.NewBorder(nil, last, nil, nil, newPad(show)))
	bellui.ShowAndRun(w)
}