Go GUI: Trying out Fyne
Tags: go, fyne, gui
Published: 2022-10-09 20:33:59

The idea of building a piece of software once and run it everywhere is certainly charming. And Go supports cross-compiling out of the box.

[Fyne](https://fyne.io/) is a very promising GUI package for Go that helps you build a GUI app and cross-compile it all devices and OSes. 

## Heres is a simple example

This code gives you a small window with a label and input field. Grab this piece of code, init a mod and tidy it, and you are good to go.

```go
package main

import (
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/widget"
	"fyne.io/fyne/v2/container"
)

func main() {
	a := app.New()
	w := a.NewWindow("Hello, you!")

	l := widget.NewLabel("Hello! What is your name?")
	e := widget.NewEntry()
	e.SetPlaceHolder("Input your name...")

	w.SetContent(container.NewVBox(l, e))
	w.ShowAndRun()
}
```

First, we define an app in `a`. Then we define a window `w` with title “Hello, you!”. 

Then we create a new label and a new input field (entry). These are all “widgets”. So their constructors are inside the `widget` package. We set a placeholder for the input field for good measure.

Next we create a vertical box (which is a container) and set it as the content of the window `w`.

When we build and run this program, this is how it looks like:

<img src="https://tonghe.xyz/assets/221009-fyne-resize-cjk-tofu.gif" style="width: 550px; height: auto;" />

You might have noticed the quirks. When you resize the window, you can see the content shivers. This is because Fyne tries to automatically adjust the window size and layout.  

And when you try to display something that's not rendered by the default typeface, it becomes a block. CJK characters, Arabic letters and emojis are all mangled.

## Using a custom font

In order to correctly display Unicode characters, you need to bundle a custom font and include it in your custom theme. Then this font is applied globally to your app as part of the theme. From what I read the issues section, as of now [only TTF is supported](https://github.com/fyne-io/fyne/issues/3245#issuecomment-1239283988). 

I downloaded [Source Han Serif](https://source.typekit.com/source-han-serif/) in TTF format. And ran this command to [bundle](https://developer.fyne.io/extend/bundle) it as a resource. 

```bash
fyne bundle SourceHanSerifSC.ttf > bundled.go
```

Next thing we do is [get a theme](https://github.com/fynelabs/notes/blob/main/theme.go) from one of Fyne's demo apps – “Notes”. 

The only thing we need to do is change the name of the font in `Font()` method in `theme.go`:

```go
func (m *myTheme) Font(s fyne.TextStyle) fyne.Resource {
	return resourceSourceHanSerifSCTtf
}
```

See? Not bad.

<img src="https://tonghe.xyz/assets/221009-fyne-data-binding.gif" style="width: 550px; height: auto;" />

## Data-binding

I grabbed the theme from Fyne Notes and didn't change the color. That explains the yellow background color. New theme is applied by calling the `SetTheme()` method.

In this example, whenever the user types in or clears the text field, the greeting changes. This is done by setting up data-binding. (If the action to link two things is “binding”, then each of the two things are “bound” together. I guess I'll call them “bound variables” and “bound widgets”.)

First we need to declare bound string variables. Then we need to use these variables to create bound widgets. Note you use *different* methods to create bound widgets.

At this point, we need to link the two bound variables. There are methods that allow bi-directional conversion between a number and a string. Some conversion methods even support string formats. There's also a method to convert strings to and from a URI.

But it seems the only way to change `greeting` whenever `userinput` changes is adding a listener. To add a listener, you need to create a data listener from an anonymous function. 

With this, whenever `userinput` changes, the callback function is executed. Notice how we `get` and `set` the value of a bound variable.

```go
package main

import (
	"fmt"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/widget"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/data/binding"
)

func main() {
	a := app.New()
    // Setting the theme. `myTheme` is defined in `theme.go`
	a.Settings().SetTheme(&myTheme{})
	w := a.NewWindow("你好 Hello")
	
    // Declare two binding string variables
	greeting := binding.NewString()
    userinput := binding.NewString()

    // Adding a listener
	userinput.AddListener(
		binding.NewDataListener(func() {
			if val, ok := userinput.Get(); val == "" || ok != nil {
				greeting.Set(
                    "你叫什么名字？\nWhat is your name?"
                    )
			} else {
				greeting.Set(
                    fmt.Sprintf("你好，%s！\nHello, %s!", val, val)
                    )
			}
		}))
	
    // Creating binding widgets
	l := widget.NewLabelWithData(greeting)
	e := widget.NewEntryWithData(userinput)

	w.SetContent(container.NewVBox(l, e))
	w.ShowAndRun()
}
```

## A few words

I tried two other pure-Go GUI packages. They are [Gio](https://gioui.org/) and [Nuxui](https://nuxui.org/). Both are ingenious projects. (There are quite a few other packages that help bind Go apps to more established GUI engines.)

Gio seems much more flexible than Fyne. And the way it uses contexts and channels seems more Go-ish. But typing Chinese in the input field simply doesn't work. The way an IME for CJK works is you type a sequence of letters, a popup menu shows up, from which you to choose a character or a word. Ideally, when an IME is active, keydown events should not be registered. The input field should only get data when a word has been composed in IME and is “committed” (selected by the user). But when you type in Gio's default input field, each letter in the sequence, as well the committed word, are all registered in the input field. This probably has to do with how Gio handles keyboard events. 

Nuxui uses a backtick-wrapped string to declaratively define the UI. This is way less cumbersome than calling a bunch of nested constructors. I didn't spend too much time on it as breaking changes are introduced to some basic functions from v0.6 to 0.8. This resulted in some sample projects failing to compile. 

I spent the most time on Fyne because Fyne is the best documented among the three. There are a few demo videos and [even a book](https://github.com/PacktPublishing/Building-Cross-Platform-GUI-Applications-with-Fyne)!

Fyne is designed with unit testing in mind. It provides helper functions to mock events. It supports comparing to snapshots to ensure correct rendering of UI.

Fyne is also well structured. Although so much so that it occasionally feels there's too much boilerplate.

But all in all, Fyne is great and very promising. I had a great time playing with it. I'm sure you will too.