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
| Interface | Methods | Delivered when |
|---|---|---|
Tappable | Tapped(*PointEvent) | A click or tap is pressed and released on the object |
DoubleTappable | DoubleTapped(*PointEvent) | Two taps in quick succession |
SecondaryTappable | TappedSecondary(*PointEvent) | A right-click on desktop; on touch, see below |
LongPressable | LongPressed(*PointEvent) | The pointer is held down without moving |
Hoverable | HoverIn, HoverMoved, HoverOut | A mouse pointer enters, moves over, or leaves the object |
Draggable | Dragged(*DragEvent), DragEnd() | The pointer moves while pressed, and then is released |
Scrollable | Scrolled(*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.
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:
| Gesture | Desktop and web | Touch |
|---|---|---|
| Tap | Primary button click | Finger tap |
| Secondary tap | Right-click | Lifting the finger after a press and hold |
| Long press | Holding the primary button down; it fires while the button is still down, and the release does not also deliver Tapped | Holding a finger still; it fires while the finger is still down |
| Hover | Mouse movement | Not delivered: there is no pointer between touches |
| Scroll | Mouse wheel or touchpad | Dragging 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:
| Method | Called 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:
- a matching shortcut in the window’s main menu;
- the focused object, if it implements
bellui.Shortcutable(TypedShortcut); - 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)
}