skel

module
v0.0.0-...-3c148fd Latest Latest
Warning

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

Go to latest
Published: Jan 31, 2025 License: MIT, Unlicense

README

skel

Go Reference

Skel is an experimental application framework for Gio developed on behalf of Plato Team and generously open-sourced for the benefit of the ecosystem.

It has been through a number of revisions as we've worked to learn how to build larger and more composable Gio applications.

The current iteration adds package stream, providing a general purpose way to consume data from asynchronous sources directly within layout.

Additionally, stream/sqlitestream provides logic for building reactive data streams atop SQLite databases. Combined with stream, it provides an end-to-end system for consuming and updating SQLite databases from Gio UIs.

Streams

Your GUI wants to display information from asynchronous sources sometimes, but it can be difficult to connect asynchronous data with the layout goroutine without generating race conditions or displaying stale data. A stream is essentially a special varible that you can read from the layout goroutine without blocking. The variable will always provide the most recent results of an asynchronous computation pipeline. This allows you to write layout code depending upon the results from a stream and receive the latest data each frame.

Streams powering visible widgets must be read from every frame. We don't want to waste resources on streams that update widgets which aren't being displayed, so streams are designed to shut down when their widget is not visible. We determine this by checking (once per frame) which streams have been read since the last check. Any stream that has not been read during the frame will go inert until it is read again.

Streams for any given application window are powered by a stream.Controller. This type connects the managed streams to the frame lifecycle of the window.

Construct a controller with:

func NewController(ctx context.Context, invalidator func()) *Controller

The provided ctx can be cancelled to shut down all streaming work for the window, and the provided invalidator function will be used by the controller to ensure the window generates a new frame when active streams emit new values. Proper use might look like this;

func loop(w *app.Window) error {
	// Make a context that lives as long as the window.
	windowCtx, cancel := context.WithCancel(context.Background())
	defer cancel()
	// Make a controller for this window.
	controller := stream.NewController(windowCtx, w.Invalidate)

	var ops op.Ops
	for {
		switch event := w.Event().(type) {
		case app.DestroyEvent:
			return event.Err
		case app.FrameEvent:
			gtx := app.NewContext(&ops, event)

			// Your layout here, passing the controller so that code can instantiate streams with it.
			layoutUI(gtx, controller)

			event.Frame(gtx.Ops)

			// Transition any active unread streams to be inactive.
			controller.Sweep()
		}
	}
	return nil
}

A stream is created like so:

// Make a stream that will emit increasing integers every second.
myStream := stream.New(controller, func(ctx context.Context) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		ticker := time.NewTicker(time.Second)
		defer ticker.Stop()
		ticks := 0
		for {
			select {
			case <-ticker.C:
				ticks++
				out <- ticks
			case <- ctx.Done():
				return
			}
		}
	}()
	return out
})

The parameters for the constructor are the controller which will manage the stream's lifecycle (connecting this stream to the parent window and ensuring that it shuts down correctly when not in use) and a function used to start the stream when it is active. That function is provided with a context that will be cancelled when the stream goes inert, and is expected to return a receive-only channel of values that will be closed when the context is cancelled. Within this function, you can perform arbitrary asynchronous computation, returning the final channel in a complex asynchronous pipeline.

To read a stream, you can invoke one of several methods prefixed with Read.

ticks, status := myStream.Read(gtx)
if status == stream.Waiting {
	// We haven't received a value over the stream yet.
} else {
	// We have a value, so do something with ticks.
}

If the exact status of the stream isn't important for your purposes, the ReadDefault and ReadInto methods are more ergonomic.

ticks := myStream.ReadDefault(gtx, 0)
var ticks int // Assume we declared this elsewhere, perhaps as a field.
myStream.ReadInto(gtx, &ticks, 0)

Here's a complete example that displays a label counting up once each second:

func tickStreamProvider(ctx context.Context) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		ticker := time.NewTicker(time.Second)
		ticks := 0
		for {
			select {
			case <-ticker.C:
				ticks++
				out <- ticks
			case <-ctx.Done():
				return
			}
		}
	}()
	return out
}

func loop(w *app.Window) error {
	// Make a context that lives as long as the window.
	windowCtx, cancel := context.WithCancel(context.Background())
	defer cancel()
	// Make a controller for this window.
	controller := stream.NewController(windowCtx, w.Invalidate)
	tickStream := stream.New(controller, tickStreamProvider)

	th := material.NewTheme()
	th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
	ticks := 0

	var ops op.Ops
	for {
		switch event := w.Event().(type) {
		case app.DestroyEvent:
			return event.Err
		case app.FrameEvent:
			gtx := app.NewContext(&ops, event)

			tickStream.ReadInto(gtx, &ticks, 0)
			material.H1(th, strconv.Itoa(ticks)).Layout(gtx)

			event.Frame(gtx.Ops)

			// Transition any active unread streams to be inactive.
			controller.Sweep()
		}
	}
}

You can try this example yourself with go run ./example/readme/stream.

For a more sophisticated example (simulating the users list of a chat application with users going on/offline, changing their usernames, etc...) see go run ./example/readme/stream2 and the corresponding source code.

See the package godoc for more detailed usage info and special types making error handling easier.

The stream API is a result of collaboration among Pedro Leite Rocha, Jack Mordaunt, and Chris Waldon. Thanks to Pedro for asking why we couldn't have nicer things.

Directories

Path Synopsis
Package bus provides a thread-safe message passing mechanism for applications.
Package bus provides a thread-safe message passing mechanism for applications.
Package future provides tools for scheduling asynchronous work and collecting the results synchronously from an event loop.
Package future provides tools for scheduling asynchronous work and collecting the results synchronously from an event loop.
Package router helps manage the content of Gio windows by routing between multiple content "pages".
Package router helps manage the content of Gio windows by routing between multiple content "pages".
Package stream provides the ability to safely read asynchronous, dynamic application state from Gio layout code.
Package stream provides the ability to safely read asynchronous, dynamic application state from Gio layout code.
automaton
Package automaton defines streamable finite state machines that allow UIs to easily model stateful, multi-step application flows.
Package automaton defines streamable finite state machines that allow UIs to easily model stateful, multi-step application flows.
sqlitestream
Package sqlitestream provides a streamable SQLite database abstraction.
Package sqlitestream provides a streamable SQLite database abstraction.
sqlitestream/mattn_sqlx_stream
Package mattn_sqlx_stream provides a concrete example of building a streaming SQLite API using the popular mattn sqlite3 driver and sqlx query helpers.
Package mattn_sqlx_stream provides a concrete example of building a streaming SQLite API using the popular mattn sqlite3 driver and sqlx query helpers.
sqlitestream/mattn_stdlib_stream
Package mattn_stdlib_stream provides a concrete example of building a streaming SQLite API using the popular mattn sqlite3 driver and Go stdlib database/sql package.
Package mattn_stdlib_stream provides a concrete example of building a streaming SQLite API using the popular mattn sqlite3 driver and Go stdlib database/sql package.
sqlitestream/sqlitewatch
Package sqlitewatch implements low-level SQLite operation watching.
Package sqlitewatch implements low-level SQLite operation watching.
Package window provides types for manipulating Gio windows within skel.
Package window provides types for manipulating Gio windows within skel.

Jump to

Keyboard shortcuts

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