Extending a widget
To change one thing about a standard widget, embed it in your own struct and add or replace methods. Your type keeps the widget’s look, its fields and its methods, and adds whatever you define.
Add a behaviour: a tappable icon
widget.Icon
shows an image but does not respond to taps. Embedding it and adding a
Tapped method makes it Tappable:
// tappableIcon is a widget.Icon that also responds to taps.
type tappableIcon struct {
widget.Icon
onTapped func()
}
func newTappableIcon(res bellui.Resource, tapped func()) *tappableIcon {
icon := &tappableIcon{onTapped: tapped}
// Pass the outer type, so the toolkit sees tappableIcon (and its
// Tapped method) rather than the embedded widget.Icon.
icon.ExtendBaseWidget(icon)
icon.SetResource(res)
return icon
}
// Tapped adds event.Tappable to what widget.Icon already does.
func (t *tappableIcon) Tapped(*event.PointEvent) {
t.onTapped()
}The important line is icon.ExtendBaseWidget(icon). Call it with your outer
type, before anything else uses the widget, and in place of the embedded
widget’s constructor (widget.NewIcon here). Bellui finds the event methods
by looking at the object it was given in ExtendBaseWidget. If it only knew
about the inner widget.Icon, it would never see Tapped.
That is also why the constructor calls SetResource after
ExtendBaseWidget, rather than calling widget.NewIcon: NewIcon would
register the inner icon, and the outer type’s methods would be ignored.
Change a behaviour: an entry that takes only digits
Define a method with the same name as one of the widget’s, and yours is called instead. To keep the original behaviour for some input, call the embedded widget’s method explicitly:
// digitsEntry is a widget.Entry that ignores typed characters other than digits.
type digitsEntry struct {
widget.Entry
}
func newDigitsEntry() *digitsEntry {
e := &digitsEntry{}
e.ExtendBaseWidget(e)
return e
}
// TypedRune replaces the Entry's method; call the original for the runes
// you accept.
func (e *digitsEntry) TypedRune(r rune) {
if unicode.IsDigit(r) {
e.Entry.TypedRune(r)
}
}widget.Entry
receives typed characters through TypedRune. The replacement passes digits
on to e.Entry.TypedRune and drops everything else.
TypedRune filters typing only. Text can still arrive by other
routes, such as pasting (a shortcut, handled by TypedShortcut) or your own
code calling SetText. If the content must be valid, also check it where
you read it, or set the entry’s Validator.Which methods you can replace
Go has no virtual methods. Your method is used when Bellui calls it
through an interface on your object: the event methods (Tapped,
TypedRune, TypedKey, Dragged and the others in
Input events), and the core.Widget methods
such as CreateRenderer and MinSize.
Your method is not used when the embedded widget calls a method on
itself. Inside widget.Entry, a call to e.TypedRune means the Entry’s own
method, not yours. So replacing a method changes how the widget reacts to
the toolkit, not how it works internally.
When the change you want is in how a widget looks or lays itself out, an extension is usually the wrong tool. Wrap the widget in a container or build a new widget that uses it.
The whole program
// Two widgets made by extending existing ones: an icon that can be tapped,
// and an entry that only accepts digits.
package main
import (
"unicode"
"github.com/belluikit/bellui"
"github.com/belluikit/bellui/app"
"github.com/belluikit/bellui/container"
"github.com/belluikit/bellui/event"
"github.com/belluikit/bellui/theme"
"github.com/belluikit/bellui/widget"
)
// tappableIcon is a widget.Icon that also responds to taps.
type tappableIcon struct {
widget.Icon
onTapped func()
}
func newTappableIcon(res bellui.Resource, tapped func()) *tappableIcon {
icon := &tappableIcon{onTapped: tapped}
// Pass the outer type, so the toolkit sees tappableIcon (and its
// Tapped method) rather than the embedded widget.Icon.
icon.ExtendBaseWidget(icon)
icon.SetResource(res)
return icon
}
// Tapped adds event.Tappable to what widget.Icon already does.
func (t *tappableIcon) Tapped(*event.PointEvent) {
t.onTapped()
}
// digitsEntry is a widget.Entry that ignores typed characters other than digits.
type digitsEntry struct {
widget.Entry
}
func newDigitsEntry() *digitsEntry {
e := &digitsEntry{}
e.ExtendBaseWidget(e)
return e
}
// TypedRune replaces the Entry's method; call the original for the runes
// you accept.
func (e *digitsEntry) TypedRune(r rune) {
if unicode.IsDigit(r) {
e.Entry.TypedRune(r)
}
}
func main() {
a := app.NewWithID("com.example.extend")
w := a.NewWindow("Extending widgets")
status := widget.NewLabel("Tap the icon")
icon := newTappableIcon(theme.InfoIcon(), func() {
status.SetText("Icon tapped")
})
number := newDigitsEntry()
number.SetPlaceHolder("Digits only")
w.SetContent(container.NewVBox(container.NewHBox(icon, status), number))
bellui.ShowAndRun(w)
}