As you may have noticed, the preceding code has two issues, both relating to the Quit button. Firstly, the func() that's called does not actually exit the application. This is a current limitation of the Shiny lifecycle code. It can be worked around with a custom lifecycle, but this is not recommended due to the large amount of code that would be required. Secondly, you may notice that newButton() is a local function and not part of the widget package. One of the widgets currently missing from the toolkit list is a standard button, therefore, we must define one ourselves. This can be done by adding the code described as follows:
First, we define the custom node; it must begin by inheriting from node.LeafEmbed. We add fields for the text label it'll contain and the onClick function that should be called when it is tapped. We should also add a convenience method to construct the button. This needs to set the node.Embed.Wrapper field, as that should never be nil:
type button struct {
node.LeafEmbed
label string
onClick func()
}
func NewButton(label string, onClick func()) *button {
b := &button {label: label, onClick: onClick}
b.Wrapper = b
return b
}
To define a suitable area for the button to take up, we need to implement the Measure() function. This will update a cached size (node.Embed.MeasuredSize) that's used for the interface layout:
const buttonPad = 4
func (b *button) Measure(t *theme.Theme, widthHint, heightHint int) {
face := t.AcquireFontFace(theme.FontFaceOptions{})
defer t.ReleaseFontFace(theme.FontFaceOptions{}, face)
b.MeasuredSize.X = font.MeasureString(face, b.label).Ceil() + 2*buttonPad
b.MeasuredSize.Y = face.Metrics().Ascent.Ceil() + face.Metrics().Descent.Ceil() + 2*buttonPad
}
To display content onscreen (this actually paints to an underlying widget.Sheet described earlier), we add a PaintBase() function. For our button, we will paint a theme.Foreground colored rectangle as a base and use the theme.Background color for the text (so our button stands out from other text). Note that, before actually painting, we remove the node.MarkNeedsPaintBase mark from the object so that it will not be redrawn on the next interface redraw:
func (b *button) PaintBase(ctx *node.PaintBaseContext, origin image.Point) error {
b.Marks.UnmarkNeedsPaintBase()
face := ctx.Theme.AcquireFontFace(theme.FontFaceOptions{})
defer ctx.Theme.ReleaseFontFace(theme.FontFaceOptions{}, face)
draw.Draw(ctx.Dst, b.Rect.Add(origin).Inset(buttonPad), theme.Foreground.Uniform(ctx.Theme), image.Point{}, draw.Src)
d := font.Drawer{
Dst: ctx.Dst,
Src: theme.Background.Uniform(ctx.Theme),
Face: face,
Dot: fixed.Point26_6{X: fixed.I(b.Rect.Min.X + buttonPad), Y: fixed.I(b.Rect.Min.Y + face.Metrics().Ascent.Ceil() + buttonPad)},
}
d.DrawString(b.label)
return nil
}
Lastly, a button needs a click handler. We can implement the OnInputEvent() function so that Shiny can send events to the button. Here, we check to see whether the event's a gesture.Event, and if so, see that its type is gesture.TypeTap. If these conditions are met, and we have an onClick handler registered, then call b.onClick():
func (b *button) OnInputEvent(e interface{}, origin image.Point) node.EventHandled {
if ev, ok := e.(gesture.Event); ok {
if ev.Type == gesture.TypeTap && b.onClick != nil {
b.onClick()
}
return node.Handled
}
return node.NotHandled
}
That concludes the code required to fulfill a hello world GUI app with Shiny (the complete code is in this book's code repository). Let's now build and run the application.