gui

package module
v0.0.0-...-b040a80 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 17, 2020 License: MIT Imports: 4 Imported by: 1

README

faiface/gui GoDoc Discord

Super minimal, rock-solid foundation for concurrent GUI in Go.

Installation

go get -u github.com/faiface/gui

Currently uses GLFW under the hood, so have these dependencies.

Why concurrent GUI?

GUI is concurrent by nature. Elements like buttons, text fields, or canvases are conceptually independent. Conventional GUI frameworks solve this by implementing huge architectures: the event loop, call-backs, tickers, you name it.

In a concurrent GUI, the story is different. Each element is actually handled by its own goroutine, or event multiple ones. Elements communicate with each other via channels.

This has several advantages:

  • Make a new element at any time just by spawning a goroutine.
  • Implement animations using simple for-loops.
  • An intenstive computation in one element won't block the whole app.
  • Enables decentralized design - since elements communicate via channels, multiple communications may be going on at once, without any central entity.

Examples

Image Viewer Paint Pexeso
Image Viewer Screenshot Paint Screenshot Pexeso Screenshot

What needs getting done?

This package is solid, but not complete. Here are some of the things that I'd love to get done with your help:

  • Get rid of the C dependencies.
  • Support multiple windows.
  • Mobile support.
  • A widgets/layout package.

Contributions are highly welcome!

Overview

The idea of concurrent GUI pre-dates Go and is found in another language by Rob Pike called Newsqueak. He explains it quite nicely in this talk. Newsqueak was similar to Go, mostly in that it had channels.

Why the hell has no one made a concurrent GUI in Go yet? I have no idea. Go is a perfect language for such a thing. Let's change that!

This package is a minimal foundation for a concurrent GUI in Go. It doesn't include widgets, layout systems, or anything like that. The main reason is that I am not yet sure how to do them most correctly. So, instead of providing a half-assed, "fully-featured" library, I decided to make a small, rock-solid package, where everything is right.

So, how does this work?

The main idea is that different components of the GUI (buttons, text fields, ...) run concurrently and communicate using channels. Furthermore, they receive events from an object called environment and can draw by sending draw commands to it.

Here's Env, short for environment:

type Env interface {
	Events() <-chan Event
	Draw() chan<- func(draw.Image) image.Rectangle
}

It's something that produces events (such as mouse clicks and key presses) and accepts draw commands.

Closing the Draw() channel destroys the environment. When destroyed (either by closing the Draw() channel or by any other reason), the environment will always close the Events() channel.

As you can see, a draw command is a function that draws something onto a draw.Image and returns a rectangle telling which part got changed.

If you're not familiar with the "image" and the "image/draw" packages, go read this short entry in the Go blog.

Draw

Yes, faiface/gui uses CPU for drawing. You won't make AAA games with it, but the performance is enough for most GUI apps. The benefits are outstanding, though:

  1. Drawing is as simple as changing pixels.
  2. No FPS (frames per second), results are immediately on the screen.
  3. No need to organize the API around a GPU library, like OpenGL.
  4. Use all the good packages, like "image", "image/draw", "golang.org/x/image/font" for fonts, or "github.com/fogleman/gg" for shapes.

What is an Event? It's an interface:

type Event interface {
	String() string
}

This purpose of this interface is to hold different kinds of events and be able to discriminate among them using a type switch.

Examples of concrete Event types are: gui.Resize, win.WiClose, win.MoDown, win.KbType (where Wi, Mo, and Kb stand for window, mouse, and keyboard, respectively). When we have an Event, we can type switch on it like this:

switch event := event.(type) {
case gui.Resize:
    // environment resized to event.Rectangle
case win.WiClose:
    // window closed
case win.MoMove:
    // mouse moved to event.Point
case win.MoDown:
    // mouse button event.Button pressed on event.Point
case win.MoUp:
	// mouse button event.Button released on event.Point
case win.MoScroll:
	// mouse scrolled by event.Point
case win.KbType:
    // rune event.Rune typed on the keyboard
case win.KbDown:
    // keyboard key event.Key pressed on the keyboard
case win.KbUp:
    // keyboard key event.Key released on the keyboard
case win.KbRepeat:
    // keyboard key event.Key repeated on the keyboard (happens when held)
}

This shows all the possible events that a window can produce.

The gui.Resize event is not from the package win because it's not window specific. In fact, every Env guarantees to produce gui.Resize as its first event.

How do we create a window? With the "github.com/faiface/gui/win" package:

// import "github.com/faiface/gui/win"
w, err := win.New(win.Title("faiface/win"), win.Size(800, 600), win.Resizable())

The win.New constructor uses the functional options pattern by Dave Cheney. Unsurprisingly, the returned *win.Win is an Env.

Due to stupid limitations imposed by operating systems, the internal code that fetches events from the OS must run on the main thread of the program. To ensure this, we need to call mainthread.Run in the main function:

import "github.com/faiface/mainthread"

func run() {
    // do everything here, this becomes the new main function
}

func main() {
    mainthread.Run(run)
}

How does it all look together? Here's a simple program that displays a nice, big rectangle in the middle of the window:

package main

import (
	"image"
	"image/draw"

	"github.com/faiface/gui/win"
	"github.com/faiface/mainthread"
)

func run() {
	w, err := win.New(win.Title("faiface/gui"), win.Size(800, 600))
	if err != nil {
		panic(err)
	}

	w.Draw() <- func(drw draw.Image) image.Rectangle {
		r := image.Rect(200, 200, 600, 400)
		draw.Draw(drw, r, image.White, image.ZP, draw.Src)
		return r
	}

	for event := range w.Events() {
		switch event.(type) {
		case win.WiClose:
			close(w.Draw())
		}
	}
}

func main() {
	mainthread.Run(run)
}
Muxing

When you receive an event from the Events() channel, it gets removed from the channel and no one else can receive it. But what if you have a button, a text field, four switches, and a bunch of other things that all want to receive the same events?

That's where multiplexing, or muxing comes in.

Mux

A Mux basically lets you split a single Env into multiple ones.

When the original Env produces an event, Mux sends it to each one of the multiple Envs.

When any one of the multiple Envs receives a draw function, Mux sends it to the original Env.

To mux an Env, use gui.NewMux:

mux, env := gui.NewMux(w)

Here we muxed the window Env stored in the w variable.

What's that second return value? That's the master Env. It's the first environment that the mux creates for us. It has a special role: if you close its Draw() channel, you close the Mux, all other Envs created by the Mux, and the original Env. But other than that, it's just like any other Env created by the Mux.

Don't use the original Env after muxing it. The Mux is using it and you'll steal its events at best.

To create more Envs, we can use mux.MakeEnv():

For example, here's a simple program that shows four white rectangles on the screen. Whenever the user clicks on any of them, the rectangle blinks (switches between white and black) 3 times. We use Mux to send events to all of the rectangles independently:

package main

import (
	"image"
	"image/draw"
	"time"

	"github.com/faiface/gui"
	"github.com/faiface/gui/win"
	"github.com/faiface/mainthread"
)

func Blinker(env gui.Env, r image.Rectangle) {
	// redraw takes a bool and produces a draw command
	redraw := func(visible bool) func(draw.Image) image.Rectangle {
		return func(drw draw.Image) image.Rectangle {
			if visible {
				draw.Draw(drw, r, image.White, image.ZP, draw.Src)
			} else {
				draw.Draw(drw, r, image.Black, image.ZP, draw.Src)
			}
			return r
		}
	}

	// first we draw a white rectangle
	env.Draw() <- redraw(true)

	for event := range env.Events() {
		switch event := event.(type) {
		case win.MoDown:
			if event.Point.In(r) {
				// user clicked on the rectangle
				// we blink 3 times
				for i := 0; i < 3; i++ {
					env.Draw() <- redraw(false)
					time.Sleep(time.Second / 3)
					env.Draw() <- redraw(true)
					time.Sleep(time.Second / 3)
				}
			}
		}
	}

	close(env.Draw())
}

func run() {
	w, err := win.New(win.Title("faiface/gui"), win.Size(800, 600))
	if err != nil {
		panic(err)
	}

	mux, env := gui.NewMux(w)

	// we create four blinkers, each with its own Env from the mux
	go Blinker(mux.MakeEnv(), image.Rect(100, 100, 350, 250))
	go Blinker(mux.MakeEnv(), image.Rect(450, 100, 700, 250))
	go Blinker(mux.MakeEnv(), image.Rect(100, 350, 350, 500))
	go Blinker(mux.MakeEnv(), image.Rect(450, 350, 700, 500))

	// we use the master env now, w is used by the mux
	for event := range env.Events() {
		switch event.(type) {
		case win.WiClose:
			close(env.Draw())
		}
	}
}

func main() {
	mainthread.Run(run)
}

Just for the info, closing the Draw() channel on an Env created by mux.MakeEnv() removes the Env from the Mux.

What if one of the Envs hangs and stops consuming events, or if it simply takes longer to consume them? Will all the other Envs hang as well?

They won't, because the channels of events have unlimited capacity and never block. This is implemented using an intermediate goroutine that handles the queueing.

Events

And that's basically all you need to know about faiface/gui! Happy hacking!

A note on race conditions

There is no guarantee when a function sent to the Draw() channel will be executed, or if at all. Look at this code:

pressed := false

env.Draw() <- func(drw draw.Image) image.Rectangle {
	// use pressed somehow
}

// change pressed somewhere here

The code above has a danger of a race condition. The code that changes the pressed variable and the code that uses it may run concurrently.

My advice is to never enclose a shared variable in a drawing function.

Instead, you can do this:

redraw := func(pressed bool) func(draw.Image) image.Rectangle {
	return func(drw draw.Image) image.Rectangle {
		// use the pressed argument
	}
}

pressed := false

env.Draw() <- redraw(pressed)

// changing pressed here doesn't cause race conditions

Credit

The cutest ever pictures are all drawn by Tori Bane! You can see more of her drawings and follow her on Instagram here.

Licence

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func MakeEventsChan

func MakeEventsChan() (<-chan Event, chan<- Event)

MakeEventsChan implements a channel of events with an unlimited capacity. It does so by creating a goroutine that queues incoming events. Sending to this channel never blocks and no events get lost.

The unlimited capacity channel is very suitable for delivering events because the consumer may be unavailable for some time (doing a heavy computation), but will get to the events later.

An unlimited capacity channel has its dangers in general, but is completely fine for the purpose of delivering events. This is because the production of events is fairly infrequent and should never out-run their consumption in the long term.

func NewMux

func NewMux(env Env) (mux *Mux, master Env)

NewMux creates a new Mux that multiplexes the given Env. It returns the Mux along with a master Env. The master Env is just like any other Env created by the Mux, except that closing the Draw() channel on the master Env closes the whole Mux and all other Envs created by the Mux.

Types

type Env

type Env interface {
	Events() <-chan Event
	Draw() chan<- func(draw.Image) image.Rectangle
}

Env is the most important thing in this package. It is an interactive graphical environment, such as a window.

It has two channels: Events() and Draw().

The Events() channel produces events, like mouse and keyboard presses, while the Draw() channel receives drawing functions. A drawing function draws onto the supplied draw.Image, which is the drawing area of the Env and returns a rectangle covering the whole part of the image that got changed.

An Env guarantees to produce a "resize/<x0>/<y0>/<x1>/<y1>" event as its first event.

The Events() channel must be unlimited in capacity. Use MakeEventsChan() to create a channel of events with an unlimited capacity.

The Draw() channel may be synchronous.

Drawing functions sent to the Draw() channel are not guaranteed to be executed.

Closing the Draw() channel results in closing the Env. The Env will subsequently close the Events() channel. On the other hand, when the Events() channel gets closed the user of the Env should subsequently close the Draw() channel.

type Event

type Event interface {
	String() string
	GetKey() int
}

Event is something that can happen in an environment.

This package defines only one kind of event: Resize. Other packages implementing environments may implement more kinds of events. For example, the win package implements all kinds of events for mouse and keyboard.

type Mux

type Mux struct {
	// contains filtered or unexported fields
}

Mux can be used to multiplex an Env, let's call it a root Env. Mux implements a way to create multiple virtual Envs that all interact with the root Env. They receive the same events and their draw functions get redirected to the root Env.

func (*Mux) MakeEnv

func (mux *Mux) MakeEnv() Env

MakeEnv creates a new virtual Env that interacts with the root Env of the Mux. Closing the Draw() channel of the Env will not close the Mux, or any other Env created by the Mux but will delete the Env from the Mux.

type Resize

type Resize struct {
	image.Rectangle
}

Resize is an event that happens when the environment changes the size of its drawing area.

func (Resize) GetKey

func (r Resize) GetKey() int

func (Resize) String

func (r Resize) String() string

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL