22 Apr 2024 - by 'Maurits van der Schee'
Last month I wrote Minesweeper written in Go using Fyne. It was a port of the Ebiten game engine implementation to desktop using the Fyne GUI library. During the implementation I ran into 3 problems that I eventually solved. In this post I'll explain what they were and how I solved them.
Source code: https://github.com/mevdschee/fyne-mines
Fyne can use a (JWT-like) borderlayout, but you can also freely place your controls (or "widgets" as Fyne calls them) using a container without a layout. To set the size of this container you can put this container in a a stack container together with an image of a certain (minimum) size.
parentContainer := container.NewStack()
i := canvas.NewImageFromImage(image.NewNRGBA(image.Rect(0, 0, 0, 0)))
i.SetMinSize(fyne.NewSize(float32(width), float32(height)))
parentContainer.Add(i)
childContainer := container.NewWithoutLayout()
parentContainer.Add(childContainer)
// add and move widgets in the childContainer
As you can see you can nest containers to achieve complex layouts.
I wanted large pixels (size 2x2), but could only achieve smoothed results. When I was setting "ScaleMode = canvas.ImageScalePixels" on a image created using the "SubImage()" method it was not rendered. If I used "draw.Copy()" or the more powerful "draw.NearestNeighbor.Scale()" on an empty image created with "image.NewNRGBA()" it did work, see:
srcRect := image.Rect(srcX, srcY, srcX+srcWidth, srcY+srcHeight)
dstRect := image.Rect(0, 0, srcWidth, srcHeight)
dst := image.NewNRGBA(dstRect)
draw.Copy(dst, image.Point{}, src, srcRect, draw.Over, nil)
img := canvas.NewImageFromImage(dst)
img.ScaleMode = canvas.ImageScalePixels
NB: The simpler "dst := src.SubImage(srcRect)" worked, but not with this pixelated (and faster) scale mode.
Fyne is awesome as it provides many controls (or "widgets" as they are called). You get "Button", "MainMenu" end even a nice (native) "TrayIcon". But for a game I mainly want to handle mouse events on a sprite (image). To handle mouse events on a "canvas.Image" I made an "interactive.Image" as defined below:
package interactive
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/widget"
)
type Image struct {
*canvas.Image
onMouseDown func(ev *desktop.MouseEvent)
onMouseUp func(ev *desktop.MouseEvent)
onMouseIn func(ev *desktop.MouseEvent)
onMouseOut func()
onMouseMoved func(ev *desktop.MouseEvent)
}
// ensure Mousable and Hoverable
var _ desktop.Mouseable = (*Image)(nil)
var _ desktop.Hoverable = (*Image)(nil)
This is essential for allowing Mouse handlers on images. I used "fyne.NewMainMenu()" to create a main menu and listen to "desktop.MouseEvent" on the canvas images. Note that I've explicitly made a desktop application and therefor have chosen the desktop events and the main menu for optimal desktop application experience.
I have experience building (2d puzzle) games in various technologies and I have recently explored Typescript (see: TicTacToe in TypeScript) and Ebiten (see: Minesweeper written in Go using Ebiten). Typescript is great for DOM based web games and Ebiten is great for pixel based web games. With Fyne I have found a great way to build cross platform 2d puzzle games for the desktop.
PS: Liked this article? Please share it on Facebook, Twitter or LinkedIn.