Skip to content

Simplify widget creation

Andy Williams edited this page Apr 2, 2024 · 7 revisions

Note

This document is archived as it refers to a design process.

This has now been implemented with the only change that SetWidget has been named ExtendBaseWidget and it is called from within CreateRenderer in case a struct is used to create a widget instead of it's constructing function.

In an effort to reduce the code required for a simple widget it is suggested that any new widget must inherit a widget class. Those looking to make minor changes or add interactivity could extend a simple widget, such as widget.Icon or widget.Label. For any other widget they would extend a new widget.BaseWidget which is outlined further down this document.

BaseWidget could also manage the caching of renderers to avoid race conditions around the render cache in a driver.

Improvement to building widgets

The result of this would be the following change to complexity of, for example widget.Box:

--- a/widget/box.go
+++ b/widget/box.go
@@ -12,40 +12,13 @@ import (
 // Box widget is a simple list where the child elements are arranged in a single column
 // for vertical or a single row for horizontal arrangement
 type Box struct {
-       baseWidget
+       BaseWidget
        background color.Color
 
        Horizontal bool
        Children   []fyne.CanvasObject
 }
 
-// Resize sets a new size for a widget.
-// Note this should not be used if the widget is being managed by a Layout within a Container.
-func (b *Box) Resize(size fyne.Size) {
-       b.resize(size, b)
-}
-
-// Move the widget to a new position, relative to it's parent.
-// Note this should not be used if the widget is being managed by a Layout within a Container.
-func (b *Box) Move(pos fyne.Position) {
-       b.move(pos, b)
-}
-
-// MinSize returns the smallest size this widget can shrink to
-func (b *Box) MinSize() fyne.Size {
-       return b.minSize(b)
-}
-
-// Show this widget, if it was previously hidden
-func (b *Box) Show() {
-       b.show(b)
-}
-
-// Hide this widget, if it was previously visible
-func (b *Box) Hide() {
-       b.hide(b)
-}
-
 // ApplyTheme updates this box to match the current theme
 func (b *Box) ApplyTheme() {
        b.background = theme.BackgroundColor()
@@ -83,7 +56,8 @@ func (b *Box) setBackgroundColor(bg color.Color) {
 
 // NewHBox creates a new horizontally aligned box widget with the specified list of child objects
 func NewHBox(children ...fyne.CanvasObject) *Box {
-       box := &Box{baseWidget: baseWidget{}, Horizontal: true, Children: children}
+       box := &Box{Horizontal: true, Children: children}
+       box.SetWidget(box)
 
        Renderer(box).Layout(box.MinSize())
        return box
@@ -91,7 +65,8 @@ func NewHBox(children ...fyne.CanvasObject) *Box {
 
 // NewVBox creates a new vertically aligned box widget with the specified list of child objects
 func NewVBox(children ...fyne.CanvasObject) *Box {
-       box := &Box{baseWidget: baseWidget{}, Horizontal: false, Children: children}
+       box := &Box{Horizontal: false, Children: children}
+       box.SetWidget(box)
 
        Renderer(box).Layout(box.MinSize())
        return box

This is mostly removal of boilerplate code (yay!) but does require that any widget constructor call SetWidget(self) so that the inherited object can make appropriate callbacks. This isn't a big impact but it does mean that if a developer had used the &widget.Box{Horizontal: false} form then they would have to call SetWidget() additionally.

Impact for extending objects

This provides the additional benefit that for simple extension of existing widgets you could simply use the following code:

type clickableIcon struct {
    widget.Icon
}

func (c *clickableIcon) Tapped(_ *fyne.PointEvent) {
    // handle tapped
}

func (c *clickableIcon) TappedSecondary(_ *fyne.PointEvent) {
    // handle secondary tapped (right click)
}

Any custom code can be contained within the Tapped() callback and you have created a clickable icon widget.