window: [hotfix] drain bus conn on window close
deps: update to latest gio and gio-x
future: provide a convenient variant using a default context
Skel is an experimental application framework for Gio.
There is a non-trivial example application in the ./example
folder demonstrating its use.
Skel provides several useful abstractions for building Gio applications. They are designed to work together, but can be used a la carte as well.
The interfaces in the bus
package provide applications with two important capabilities:
A tricky aspect Gio development has always been managing the interactions between the state that an application is presenting and modification to that state. Performing expensive state updates (like HTTP requests to an API) on the layout goroutine results in terrible UI performance. Directly modifying the state of the UI from a new goroutine doing the state update results in race conditions.
The bus.Connection
type provides the Loopback
, Broadcast
, and Omnicast
methods as threadsafe mechanisms to send messages from any goroutine to just one connection, every connection but the one in use, or all connections (respectively). Your application can invoke expensive state updates on new goroutines, send the results with these methods, and then wait for the results of the state update to arrive over the bus.Connection.Output()
channel.
Here's a simple Gio window event loop making use of this technique to choose a number asynchronously and add it to the current window's state:
type NewNumberEvent int
func loop(w *app.Window, conn bus.Connection) error {
th := material.NewTheme(gofont.Collection())
var (
ops op.Ops
add widget.Clickable
)
currentNumber := 5
for {
select {
case event := <-w.Events():
switch event := event.(type) {
case system.DestroyEvent:
return event.Err
case system.FrameEvent:
if add.Clicked() {
// Schedule asynchronous work to fetch an new number. In real applications,
// these are often expensive and blocking operations.
go func() {
// Sleep to simulate expensive work like database
// interactions, I/O, etc...
time.Sleep(time.Millisecond * 200)
conn.Loopback(NewNumberEvent(rand.Intn(100) + 1))
}()
}
// Lay out the UI here
gtx := layout.NewContext(&ops, event)
layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(
material.H1(th, strconv.Itoa(currentNumber)).Layout,
),
layout.Rigid(material.Button(th, &add, "Add").Layout),
)
event.Frame(&ops)
}
case update := <-conn.Output():
// We got an update from the bus. Update application state
// accordingly.
switch update := update.(type) {
case NewNumberEvent:
currentNumber += int(update)
w.Invalidate()
}
}
}
}
You can run the above example with go run ./example/readme/bus
.
Applications should construct a bus.Bus
in main
(or at startup), and then pass each window a bus.Connection
from that bus's Connect()
method. Orchestrating those connections for multiple windows might sound like a pain, but the next abstraction makes it a breeze.
To make multi-window Gio applications easier, skel
provides a type that manages the entire lifecycle of Gio windows. The window.Windower
is created by wrapping a bus.Bus
, and it listens to that bus and responds to window-management requests sent on the bus.
To create a window, the Windower
looks for window.CreateWindowRequest
s on the application bus. The CreateWindowRequest
provides a window.WindowFunc
and a set of app.Option
that should be provided to Gio when constructing the window. WindowFunc
's signature should be familiar from the example above: func (*app.Window, bus.Connection) error
. Any event loop function like the one above can be provided, and it will be launched in a new window.
Since constructing window.CreateWindowRequest
s isn't especially ergonomic, skel
provides the window.NewWindow
and window.NewWindowForBus
helper functions. These will construct and send the request for you, provided they are given the proper type from package bus
, a window.WindowFunc
, and (optionally) some app.Option
s.
Here's how to make the previous example into a multi-window application (code that did not change has been ellided):
func main() {
bus := bus.New()
w := window.NewWindower(bus)
go func() {
// Wait for all windows to close, then exit.
w.Run()
os.Exit(0)
}()
window.NewWindowForBus(bus, loop)
app.Main()
}
func loop(w *app.Window, conn bus.Connection) error {
// ...
var (
ops op.Ops
add, newWindow widget.Clickable // Add a new button.
)
// ...
for {
select {
case event := <-w.Events():
switch event := event.(type) {
// ...
case system.FrameEvent:
if add.Clicked() {
// ...
}
if newWindow.Clicked() {
// Launch a new window running another copy of this event
// loop.
window.NewWindow(conn, loop)
}
// Lay out the UI here (laying out the new button has been ellided)
// ...
}
case update := <-conn.Output():
// Handle any requests to modify the window that came over the bus.
window.Update(w, update)
// ...
}
}
}
You can run this example with go run ./example/readme/windower
.
Notice how only one of the windows update when you click the "Add" button in any window. Try changing the call of bus.Connection.Loopback
to bus.Connection.Omnicast
. This will send the event to all connections, not just the one you invoke it on. Every window will now increment when you click add in any window.
Another useful skel
abstraction has to do with switching the contents of a single Gio window. Often your application will have multiple "screens", "views", or "pages" of content that the user navigates between. The router.Page
interface is designed to encapsulate that idea.
router.Page
exposes two methods. One updates the state of the page by passing new state (usually read from the application bus), returning whether the page changed. The other lays out the page. That's it. Here's a simple app using a page. Note how simple the event loop has become.
func loop(w *app.Window, conn bus.Connection) error {
var ops op.Ops
page := NewMyPage(conn)
for {
select {
case event := <-w.Events():
switch event := event.(type) {
case system.DestroyEvent:
return event.Err
case system.FrameEvent:
// Lay out the UI here
gtx := layout.NewContext(&ops, event)
page.Layout(gtx)
event.Frame(&ops)
}
case update := <-conn.Output():
// Handle any requests to modify the window that came over the bus.
window.Update(w, update)
// Check for application state updates and handle them.
if page.Update(update) {
w.Invalidate()
}
}
}
}
// MessageEvent is an event indicating that a new message has
// been chosen.
type MessageEvent string
// MyPage is a simple router.Page that displays a message and
// has a button to request a new message.
type MyPage struct {
*material.Theme
message string
btn widget.Clickable
bus.Connection
}
// NewMyPage constructs a simple page that displays a message.
func NewMyPage(conn bus.Connection) *MyPage {
return &MyPage{
Connection: conn,
Theme: material.NewTheme(gofont.Collection()),
message: "hello",
}
}
// Update the page's state.
func (m *MyPage) Update(data interface{}) bool {
// Check if the data is a type we care about.
switch data := data.(type) {
default:
return false
case MessageEvent:
// Update our message.
m.message = string(data)
}
return true
}
// Layout the page.
func (m *MyPage) Layout(gtx C) D {
if m.btn.Clicked() {
// Schedule some work. We'll get the results in the Update() method.
go func() {
m.Connection.Loopback(MessageEvent("It's " + time.Now().Format("15:04:05")))
}()
}
return layout.Flex{
Axis: layout.Vertical,
Spacing: layout.SpaceAround,
}.Layout(gtx,
layout.Rigid(material.H1(m.Theme, m.message).Layout),
layout.Rigid(material.Button(m.Theme, &m.btn, "New message").Layout),
)
}
You can try this application with go run ./example/readme/singlepage/
.
We also provide a router.Router
that can display one of several router.Page
s. It maintains history so that users can go "back", and it implements router.Page
so that it can be used anywhere a page can be.
See ./example/page/ui.go
for how to use the router. go run ./example
to see a larger application leveraging all of the skel
abstractions together.