Skip to content
Extending a widget

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.

Overriding 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)
}