Newsletter, October & November 2023
Eliminating Event Processing Latency

The last two months have brought lots of change across Gio, but also kept me so busy that October’s newsletter was just folded into November’s. Since the last newsletter, we’ve fixed two major longstanding API design issues within Gio. Now GUIs can react to input events without an extra frame of latency between the event and its delivery, and also Gio window logic now runs on the same goroutine as application window logic, eliminating many opportunities for race conditions. Of course, fixing these problems did require breaking API changes, so see each repo’s notes for API migration info.

Additionally, I had the honor of presenting both a high-level introductory talk and a workshop about Gio at the Ubuntu Summit in Riga, Latvia. My recorded talks are not yet available on YouTube (they seem to only have finished with day 1’s talks), but you can find my high-level talk’s slide deck here and my workshop materials here if you’re curious. The workshop materials include an example reactive Gio todo-list application which may be of interest to authors of stateful applications.

While there are many major improvements merged as part of Gio v0.4.0, there’s another large API change proposed that should solve a number of issues with event routing, the operation list, focus management, and widget composition at the cost of more breaking changes. Please see the proposal here and add your feedback.

Sponsorship

This month, Gio thanks the following organizations and community members for their ongoing support!

Supporting the whole team:

Supporting a maintainer:

Sponsorship money given to Gio enables Elias and I to cover the costs of running Gio’s infrastructure, to pay for some of our time spent maintaining and improving Gio, and to plan for funding significant feature work. You can support Gio by contributing on OpenCollective or GitHub Sponsors.

gioui.org@v0.4.0

Since v0.3.1, Egon Elbre optimized text glyph processing, Larry Clapp fixed a bug in widget.Selectable’s key event processing, I added some observability to system font selection, and Elias overhauled the window and event APIs to fix some problems. I’ll focus on the API changes in the interest of brevity.

API Change: Window Event Iteration

Historically, Gio applications have received events for application windows over a channel returned from (*app.Window).Events(). However, running the Gio-internal window management logic in a different goroutine than your application’s window event loop introduced opportunities for subtle race conditions, especially during window shutdown. To simplify the architecture and eliminate these races, app.Window now offers a NextEvent() method instead which directly returns the next application window event (blocking if necessary).

For applications with simple event loops, the change may be as simple as replacing <-w.Events() with w.NextEvent(), but many Gio applications may have used event loop designs for which this simple substitution isn’t adequate. I’ll cover how to convert each of these next.

If you do have a simple event loop, this rewrite rule may fix it for you:

gofmt -w -r "<-w.Events() -> w.NextEvent()"

If you have an event loop of this form:

for event := range w.Events() {
	switch event := event.(type) {
	case system.FrameEvent:
	// Omitted
	case system.DestroyEvent:
	// Omitted
	}
}

You should restructure it to look like this:

for {
	switch event := w.NextEvent().(type) {
	case system.FrameEvent:
	// Omitted
	case system.DestroyEvent:
	// Omitted
	}
}

If you have an event loop with a custom select with multiple cases, things get a little more nuanced. Let’s examine the simple case of having a custom select case which just triggers window invalidation:

for {
    select {
    case event := <-w.Events():
    	switch event := event.(type) {
    	case system.FrameEvent:
    	// Omitted
    	case system.DestroyEvent:
    	// Omitted
    	}
    case <-someChannel:
    	w.Invalidate()
	}
}

This structure can be converted pretty easily by converting the extra select case into a dedicated goroutine:

go func() {
    for range someChannel {
        w.Invalidate()
    }
}()
for {
	switch event := w.NextEvent().(type) {
	case system.FrameEvent:
	// Omitted
	case system.DestroyEvent:
	// Omitted
	}
}

This works for two reasons:

  1. (*app.Window).Invalidate() is thread-safe.
  2. The logic executed by the custom select case doesn’t need access to the state of your UI when it runs.

Reason 2 is key. If you have a custom select case that needs to manipulate or check the state of your UI, you have two options for converting it. First I’ll show an example of this:

var ed widget.Editor
for {
    select {
    case event := <-w.Events():
    	switch event := event.(type) {
    	case system.FrameEvent:
    	// Omitted
    	case system.DestroyEvent:
    	// Omitted
    	}
    case newText := <-someChannel:
    	// Manipulate the state of a widget in response to a select case.
    	// Since we need to access the state of the UI, we can't just throw this
    	// in a new goroutine unchanged without introducing race conditions.
    	ed.SetText(newText)
    	w.Invalidate()
	}
}

One way to convert this is to wrap your window state in a mutex:

var ed widget.Editor
var lock sync.Mutex
go func() {
    for range someChannel {
        // We need to lock the mutex each time we want to see or manipulate the UI state
        // from this goroutine.
    	lock.Lock()
    	ed.SetText(newText)
    	lock.Unlock()
    	w.Invalidate()
    }
}()
for {
    event := w.NextEvent()
    // Whenever we're processing an event, we need to lock the mutex as well.
    lock.Lock()
	switch event := event.(type) {
	case system.FrameEvent:
	// Omitted
	case system.DestroyEvent:
		// We have to specially unlock the mutex here or we'll return from the
		// event loop with it still locked.
		lock.Unlock()
    	return event.Err
	}
	lock.Unlock()
}

This is ugly and a little error-prone, but doable. It would be cleaner to introduce some helper functions per-iteration so that you could safely use defer to handle unlocking the mutex on all return paths, but that would make the example quite long.

An alternative formulation is to use two-way channel communication to process window events using your old event loop, but ensuring that the new Gio window event loop iterates in lockstep:

// Make a channel to read window events from.
events := make(chan event.Event)
// Make a channel to signal the end of processing a window event.
acks := make(chan struct{})

go func() {
	// Iterate window events, sending each to the old event loop and waiting for
	// a signal that processing is complete before iterating again.
	for {
		ev := w.NextEvent()
		events <- ev
		<-acks
		if _, ok := ev.(system.DestroyEvent); ok {
			return
		}
	}
}()

var ed widget.Editor
for {
    select {
    case event := <-events:
    	switch event := event.(type) {
    	case system.FrameEvent:
    	// Omitted
    	case system.DestroyEvent:
    		// We must manually ack a destroy event in order to ensure that the other goroutine
    		// shuts down when we return.
			acks <- struct{}{}
        	return event.Err
    	}
    	// If we didn't get a destroy event, ack that we're finished processing the window event
    	// so that the other goroutine can continue.
		acks <- struct{}{}
    case newText := <-someChannel:
    	ed.SetText(newText)
    	w.Invalidate()
	}
}

As you can see, this construct is more code, but allows the old event loop to be used almost unchanged. It’s worth calling out that we still need to do some special handling during the processing of a destroy event in order to avoid leaking a goroutine.

There are other possible refactorings, and you should use whatever makes the most sense for your application. The critical thing is to avoid accessing your UI state from multiple goroutines without synchronization, and to ensure you are completely finished processing a window event before calling (*app.Window).NextEvent() again. It helps to always test your program with -race to catch any accidental state management mistakes.

API Change: Widget Update API

Since commit d017c722, applications have experienced a frame of latency between interactions with widgets (such as clicking on a button) and their handlers for such buttons firing (the Clicked() method on said button). You can review that commit message for the rationale behind the change three years ago. Since then, we’ve concluded that this design choice was a mistake, and have spent many months thinking on how to approach the problem differently.

For an example of the problem, look at this code snippet:

// Assume this is declared elsewhere
var btn *widget.Clickable

if btn.Clicked() {
	// Change the UI in response to the click.
}
btn.Layout(gtx)

In the pre-Gio-v0.4.0 world, here is the sequence of events:

  1. User clicks button, causing event to be enqueue in the router and the window to be invalidated
  2. The above snippet is run. btn.Clicked() executes, but the btn doesn’t yet know about the click event, so it returns false.
  3. btn.Layout executes, which performs event processing for the button and discovers that the click has occurred. The button’s internal layout can change in response to this click, but it’s too late to perform whatever changes the prior clicked check was meant to enact.
  4. Gio automatically schedules an invalidation after delivering events, so we generate another frame event after the previous one is finished.
  5. The above snippet is run for the next frame. btn.Clicked() returns true now that the button is aware of the click, and the UI can change in response to the click event.

Elias resolved this problem by dividing the process of updating the state of a widget from laying it out. Now all stateful widgets have an Update(gtx C) method that performs event processing for that widget and may return relevant state changes. The return type varies from widget to widget.

You can now call the Update(gtx) method on relevant widgets and immediately process any state changes generated before laying any of those widgets out. Note that some widget methods have been renamed to Update to provide API consistency.

The above snippet now looks like this:

// Assume this is declared elsewhere
var btn *widget.Clickable

if btn.Clicked(gtx) {
	// Change the UI in response to the click.
}
btn.Layout(gtx) 

The change is subtle in that we now pass the gtx to the Clicked method, but this means that the button can perform event processing immediately. The sequence of events is now:

  1. User clicks button, causing event to be enqueue in the router and the window to be invalidated
  2. The above snippet is run. btn.Clicked(gtx) executes, and it’s a helper wrapping btn.Update(gtx). The button performs event processing and handles the click event, causing btn.Clicked to return true. The UI changes in response to the event.
  3. btn.Layout executes, displaying the button and animating the click.

All widget Layout methods automatically invoke their Update method, so applications that do not need to query the state early do not need to do it.

Previously, Gio automatically generated an extra frame after delivering an event to ensure that the UI would get a chance to react to the event. Now that the UI can react on the same frame that the event is delivered, this is no longer the case. As a result, Gio no longer generates the extra frame. Applications relying upon an extra frame after the delivery of an event may need to invalidate manually (this should be rare).

API Change: Type -> Kind

Elias also changed all uses of the word Type or Types to use the more idiomatic Kind or Kinds. You’ll encounter this change primarily when processing events, as key, pointer, and gesture events used to used fields and constants with the term Type in them.

These gofmt rewrites will help ease the transition, but some rewriting of field names is likely required:

gofmt -w -r "pointer.Type -> pointer.Kind"
gofmt -w -r "gesture.ClickType -> gesture.ClickKind"
gofmt -w -r "gesture.TypePress -> gesture.KindPress"
gofmt -w -r "gesture.TypeClick -> gesture.KindClick"
gofmt -w -r "gesture.TypeCancel -> gesture.KindCancel"

Breaking Changes by Author

Elias Naur:

  • io/semantic: [API] replace DisabledOp with EnabledOp. The double-negative DisabledOp is harder to understand than a straightforward EnabledOp. Note that the absence of an EnabledOp implies still means that the widget is enabled. e1b39288
  • io/pointer: [API] rename PointerEvent.Type to Kind. Kind is the idiomatic field name for distinguishing a struct without using separate types. 650ccea2
  • gesture: [API] rename ClickType to ClickKind. “Kind” is the Go idiomatic name for distinguishing structs outside of the type system. 1686874d
  • widget: [API] move Clickable state update from Layout to Clicks. Before this change, Clickable state updates would happen in Layout. However, that is too late in cases where clicks affects layout that contiains the Clickable. 4a4fe5a6
  • widget: [API] rename Bool.Changed to Update and move state update to it. Similar to a previous change for Clickable, this change separates Bool state changes to its renamed method Update. This allows access to the most recent state before calling Layout. dc978711
  • widget: [API] move Decorations state update to Actions. Similar to a previous change for Clickable and Bool this change separates state changes from Decorations.Layout to Actions so that access may happen before Layout. b9837def
  • widget: [API] move Enum state update to Changed, rename it to Update. Similar to an earlier change for other widgets, this change separate Enum state changes for access earlier than Layout. fe85136f
  • widget: [API] separate state changes from Draggable.Layout to Update. 23e44292
  • widget: [API] separate Float state update; remove min, max, invert parameters. This change allows users of Float to determine its state before Layout by calling Update. d42dae73
  • io/router: [API] drop extra frame. This change removes the extra frame scheduled when events was delivered during a frame. This extra frame was intended to paper over state changes that happen later than the layout depending on it. dc170033
  • gesture: [API] rename gesture state update methods to Update. c756986d
  • app: [API] replace events channel with an iterator interface. The goroutine started by Window.run runs concurrently with the user goroutine receiving from Window.Events, leading to races such as #543. This change replaces the Window.run goroutine and the Window.Events channel with an iterator API driven by the user goroutine directly. 37717d0d

Egon Elbre:

  • text: [API] reduce size of Glyph.Runes to uint16. df8a8789

Chris Waldon:

  • widget: [API] split text widget Update from Layout. This commit introduces Update(gtx) functions for both Selectable and Editor, allowing their state to be updated explicitly prior to layout. This completes the transition that allows all Gio widgets to have their state updated ahead-of-time, ensuring that there is zero frame lag between an input event and the widget response to that event. 3fde0c00

Changes by Author

Elias Naur:

  • .builds: remove unused Chrome. Chrome was required when gogio was part of the repository. It is no longer. 7550d854
  • Revert "app: [Wayland] avoid a race on the send side of the wakeup pipe”. This reverts commit 7fde80e8050b25df4f0592c0b8d8e25b66b4645d, because Wakeup can no longer be called after the window has been destroyed. ce8475a0
  • widget: use local random source to avoid deprecated rand.Seed. This change replace the global rand use with a local source, to avoid the recently deprecated global rand.Seed function. At the same time, the time-dependent seeds are replaced with static numbers to ensure reproducible benchmarks numbers. 63fea3d2
  • app: [macOS] don’t free nil string in ReadClipboard. Fixes: #539 ea58aacd
  • app: unexport NewDisplayLink. d078bf0e

Egon Elbre:

  • internal/ops: use uint32 for pc, version, macroID. 4GB of render data should be sufficient for anyone. 49296bd0
  • text: use a simpler hash. The hash calculation is a significant bottleneck in caching, replace it with a simpler “add; multiply by a prime” approach. 62edabe1
  • widget: optimize processGlyph. processGlyph does not modify the value, so there’s no reason to return the struct. 48bd5952

Chris Waldon:

  • text: add system font loads to debug log. This commit adds a GIODEBUG=text log message each time a system font is resolved. This makes it vastly easier for application authors to determine which system fonts are being used by their application. 9d89f7c8
  • widget: test update-only editor logic. c8801fe2

Larry Clapp:

  • widget: Update Selectable key filter. Selectable was using a key event filter copied directly from editor.go, but it didn’t actually process all those keys. Update the filter to only ask for the keys that Selectable actually uses. ae2b1f42

gioui.org/x@v0.4.0

X mostly changed in order to be compatible with API changes in Gio with the occasional bugfix. The only exception is the debug package, which gained keyboard shortcuts and a means of ensuring that only one debugger is active at a time.

Changes by Author

Chris Waldon:

  • component: fix recursion in truncating label style. a4eb92d
  • debug: single debugger at a time and keyboard shortcuts. This commit ensures that only a single constraint debugger is active at a time within a given window (using keyboard focus) and adds some simple keyboard shortcuts to the editor. 2586de8
  • go.*,colorpicker,component,debug,outlay,richtext: make compatible with latest Gio. This commit updates all widget/gesture event API use to be compatible with the latest Gio APIs. e2613e1
  • outlay,richtext: fix tests to use new API. 3505fff
  • go.*: update to gio v0.4.0. e875018

gioui.org/example@v0.4.0

Example is updated to be API-compatible. A special thanks to Egon for simplifying some event loops after the API conversion.

Changes by Author

Chris Waldon:

  • go.*: fix infinite recursion crash in component example. This commit updates to gioui.org/x@v0.3.2, which contains a bugfix for an accidental recursion in the truncating label style type. b9c5ea5
  • go.*,all: update to new Gio APIs. This commit updates all examples to be compatible with changes in core APIs like the new window event function and renamed types/fields. dcf5e8d
  • galaxy: fix incorrect variable names. 027c3a0
  • go.*: update to gio and gio-x v0.4.0. 32ba1f1

Egon Elbre:

  • all: collapse w.NextEvent with switch. feb8e57
  • 7gui/timer: simplify event handling. 35cdcbe

giouiorg

Egon added a useful discussion of color and blending in Gio. Thanks Egon!

End

Thanks for reading!

Chris Waldon