Trying out Fyne
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 is a very promising GUI package for Go that helps you build a GUI app and cross-compile it for (almost) all devices and OSes.
Heres is a quick example
For a quick example, this code gives you a small window with a label and an input field. Grab this piece of code, init a mod and tidy it, and you are good to go.
package main
import (
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/widget"
"fyne.io/fyne/v2/container"
)
func main() {
a := app.New() // {1}
w := a.NewWindow("Hello, you!") // {2}
l := widget.NewLabel("Hello! What is your name?") // {3}
e := widget.NewEntry()
e.SetPlaceHolder("Input your name...")
w.SetContent(container.NewVBox(l, e)) // {4}
w.ShowAndRun()
}
First, on line {1} we define an app in a
. Then we define a window w
with title “Hello, you!” on line {2}.
Then we create a new label (line {3}) 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
on line {4}.
When we build and run this program, this is how it looks like:
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.
I downloaded Source Han Serif in TTF format. And ran this command to bundle it as a resource.
fyne bundle SourceHanSerifSC.ttf > bundled.go
Next thing we do is get a theme 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
:
func (m *myTheme) Font(s fyne.TextStyle) fyne.Resource {
return resourceSourceHanSerifSCTtf
}
See? Not bad.
Data-binding
I borrowed 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.
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 and Nuxui. 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!
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.