Newsletter, February 2024
Event Routing Overhaul

A massive, long-overdue change to Gio event routing has just landed. This provides much more granular capabilities for applications to manipulate input events and ensures that it is always safe to throw away the contents of a macro, but at the cost of some breaking changes.

Sponsorship

These past few months, 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.5.0

Elias overhauled event routing and I fixed up some text widgets to match. This involved many breaking changes that are detailed below.

Additionally, six community members made improvements to Gio in addition to the core team:

  • Dominik Honnef fixed some stale documentation and optimized allocations within package gpu.
  • James Stanley fixed a bunch of stale docs in package material.
  • Egon Elbre added nearest-neighbor filtering to paint.ImageOp and added a more efficient layout.Background type for the simple case of stacking two widgets.
  • sewn made it possible to configure the height and radius of the material progress bar.
  • Siva ensured that Gio windows raise themselves properly on macOS even when their application is not the active application.
  • Danny Wilkins fixed a startup crash on OpenBSD due to using the wrong so name for libGLESv2.

Thanks everyone for your contributions!

API Change: Commands

Previously, Gio had many ops that could be inserted into the operation list to make stateful modification to the application. For example, ops could trigger reads and writes of the clipboard, could update the IME state of the editor, and could transfer keyboard input focus.

This worked fine until such an op was inserted into a macro that was never used. It’s relatively common in complex layouts to use a macro to check the dimensions of a widget and sometimes re-layout that widget with different constraints. If these ops were generated by the first layout of the widget but not the second (a common case), throwing away the macro could break widget invariants leading to incredibly confusing widget bugs.

To ensure that it’s always safe to discard a macro’s ops, all of these application state changes have been converted into “Commands”. You can perform a command from widget/layout code with gtx.Execute(command). The most common command change is:

gofmt -w -r 'op.InvalidateOp{}.Add(gtx.Ops) -> gtx.Execute(op.InvalidateCmd{})' .

Each newly-missing op has a corresponding command in its place, but they can’t be handled by trivial rewrite rules.

API Change: Event Filters

The largest change to Gio’s API changes how events are delivered to widgets. Previously, widgets would register a pointer.InputOp or key.InputOp to declare that they wanted certain kinds of input event. Fields on those ops would describe the kinds of event that should be delivered to the provided Tag.

Now, a widget declares interest in input events for the current clipping area by calling event.Op(gtx.Ops, tag). This configures the tag as a candidate to receive input, but does not specify which kinds of events should be routed to it.

Previously, receiving events from the layout.Context involved calling its Events(tag) method. Now, you call gtx.Event(...filters) where the filters describe the kinds of events you are interested in (as well as which tags they should be routed to).

A concrete example of converting the old pointer API to the new filter API:

// Old input op
pointer.InputOp{
    Tag: myTag,
    Kinds: poiner.KindPress,
}.Add(gtx.Ops)

// Old event reading
for _, event := range gtx.Events(myTag) {
	ev, ok := event.(pointer.Event)
	if !ok {
    	continue
	}
	// handle ev
}

Converts to:

// New input op
event.Op(gtx.Ops, mytag)

// New event reading
for {
	event, ok := gtx.Event(pointer.Filter{
    	Target: myTag,
    	Kinds: pointer.KindPress,
	})
	if !ok {
		break
	}
	ev, ok := event.(pointer.Event)
	if !ok {
    	continue
	}
	// handle ev
}

Here’s a concrete example of converting the old key API to the new filter API:

// Old input op
key.InputOp{
    Tag: myTag,
    Set: key.NameEscape+"|"+key.NameEnter,
}.Add(gtx.Ops)

// Old event reading
for _, event := range gtx.Events(myTag) {
	ev, ok := event.(key.Event)
	if !ok {
    	continue
	}
	// handle ev
}

Converts to:

// New input op
event.Op(gtx.Ops, mytag)

// New event reading
for {
	event, ok := gtx.Event(
		key.FocusFilter{
			Target: myTag,
		},
    	key.Filter{
        	Focus: myTag,
        	Name: key.NameEscape,
    	},
    	key.Filter{
        	Focus: myTag,
        	Name: key.NameEnter,
    	},
	)
	if !ok {
		break
	}
	ev, ok := event.(key.Event)
	if !ok {
		continue
	}
	// handle ev
}

Note that the new API allows receiving key events regardless of the currently-focused tag if no Focus is set in the filter. If we only wanted to watch the escape and enter keys unconditionally, we could drop the input op entirely and just call:

for {
	event, ok := gtx.Event(
    	key.Filter{
        	Name: key.NameEscape,
    	},
    	key.Filter{
        	Name: key.NameEnter,
    	},
	)
	if !ok {
		break
	}
	ev, ok := event.(key.Event)
	if !ok {
		continue
	}
	// handle ev
}

This change makes it easier to write top-level key event handlers, as well as pre-filtering events so that they are not consumed by an editor widget.

Events are only ever delivered once. Widgets have been rewritten to ensure that their event delivery tag is the address of their state types. This means that it is now possible to intercept events destined for a different widget by asking for them first. For example, to intercept an arrow key on its way to an editor:

for {
	event, ok := gtx.Event(
    	key.Filter{
    		Focus: &editor,
        	Name: key.NameUpArrow,
    	},
	)
	if !ok {
		break
	}
	ev, ok := event.(key.Event)
	if !ok {
		continue
	}
	// handle ev
}

This code will be able to process the arrow key event, and the editor will never see it. For more complex use-cases, applications should check the state of the editor to decide whether to pre-filter its events.

API Change: Widget Update API

widget.Editor, widget.Selectable, and widget.Scrollbar now have separate Update(gtx layout.Context) (something) methods to match the API style of the rest of the core widgets. What their update methods return varies between widgets.

API Change: Event, NewContext, and Insets Rehoming

system.DestroyEvent, system.FrameEvent, layout.NewContext, and system.Insets have all moved to package app.

gofmt -w -r 'system.DestroyEvent -> app.DestroyEvent' .
gofmt -w -r 'system.FrameEvent -> app.FrameEvent' .
gofmt -w -r 'layout.NewContext -> app.NewContext' .
gofmt -w -r 'system.Insets -> app.Insets' .

API Change: Profile Package Deleted

The profile package exposed too many implementation details of the renderer that couldn’t be maintained stably, so it has been dropped.

Breaking Changes by Author

Elias Naur:

  • io/profile: [API] delete package. It was a design mistake to make profiling data available to programs. Rather, profiling should either be a user-configurable debug overlay, reported through runtime/trace, or both. 3648bdc0
  • io/router: [API] make SemanticID a uint, not uint64. 60bfb9e0
  • app,io/system,layout: [API] move FrameEvent and Insets to package app. In the early days of Gio, FrameEvent was part of package app. It was moved to package system to enable layout.NewContext be a convenient short-hand for constructing a layout. cb1e6052
  • app: [API] remove assumption that FrameEvent.Queue is an interface. We’re about to make the Queue field of FrameEvent (and layout.Context) a concrete type. Remove the interface assumption from app.Window. dd36ec5e
  • io/input,io/router: [API] rename package io/router to io/input. The input name better matches its purpose, in particular when we introduce input.Source. d5a0d2cf
  • layout,app: [API] rename FrameEvent.Queue and Context.Queue to Source. We’re about to replace the interface Queue with a concrete input.Source. This change renames the field accordingly. 4fcd96ac
  • io/input: [API] introduce Source, the interface between a Router and widgets. This change gets rid of the event.Queue interface by replacing it with input.Source values. Source provides the interface to Router necessary to implement interface widgets. 60275179
  • io/key,io/input: [API] move FocusDirection to package io/key. a11f35fe
  • io/input,io/key: [API] introduce Command, replace FocusOp with FocusCmd. Modeling focus change as an operation is awkward, because focus changes logically happen during event processing, not layout. In particular, you want to apply focus changes even if a widget is subsequently never laid out. be36fc88
  • io/input,io/key: [API] replace SoftKeyboardOp with a command. 5dd41f74
  • widget: [API] re-implement Editor.Focus in terms of commands. 8334d2ab
  • widget: [API] re-implement Selectable.Focus in terms of commands. 9de80749
  • widget: [API] re-implement Clickable.Focus with a command. 627e028d
  • io/input,io/key: [API] replace SelectionOp with command. 813d8366
  • io/input,io/key: [API] replace SnippetOp with command. eed93aaf
  • io/input,io/transfer: [API] replace OfferOp with command. a3c539b3
  • io/input,io/clipboard: [API] replace WriteOp with command. d51aea55
  • io/input,io/clipboard: [API] replace ReadOp with command. 676b6701
  • io/input,io/pointer: [API] make pointer grab a command. 1bcbaa81
  • io/system,app: [API] move DestroyEvent, StageEvent, Stage to package app. They’re only useful at the top-level event loop in combination with an app.Window. d2085ab7
  • io: [API] introduce event filters; convert pointer input to use them. Instead of having to supply the predicates for event filtering at the time of layout, the new Filter type allows widgets to filter at the time of calling Source.Events. There is then only the need for a single input op type, in package event. ef8171b9
  • io/key: [API] add InputHintOp for specifying the input hint for a tag. We’re about to replace key.InputOp with a filter; this change separates the input hint into its own operation. 12a0ad70
  • io/key: [API] introduce FocusFilter for matching focus and editor events. 73c3849d
  • io/key: [API] replace key.InputOp with a filter. 27ef6dd7
  • io/input: [API] execute commands immediately. Change the semantics of commands to execute immediately. In cases where execution of a command introduces a inconsistency, freeze event routing and defer the command as well as queued events to the next frame. fc208248
  • io/input: switch internal API to return one event at a time. bce1dbd6
  • all: [API] deliver events one at a time to allow fine-grained event processing. Processing one event at a time allows a widget to execute commands after the event that triggered it, instead of after all matching events. 88f5ac9c
  • widget: [API] change Clickable.Update to report one click at a time. Similar to how events are processed one at a time, change Clickable to report clicks one at a time. 0fab08bd
  • widget: [API] replace Focus methods with explicit FocusCmds. Now that widgets by convention may be focused by issuing FocusCmd directly, remove the now redundant Focus methods on Clickable, Editor, Selectable. ab9f42c8
  • all: [API] replace tag parameter of Source.Event with per-filter tags. Until now, every event has had a particular target. We’re about to simplify key event delivery to match the first matching filter, so there is no longer a global meaning to the tag argument to Source.Event. d9a00758
  • all: [API] deliver key events to the first matching filter. Replace the key.Filter.Target field with a Focus field that matches only of the specified tag has focus. This has the advantage of simpler event delivery and for lower latency in delivering key events to new handlers. ed0d5d57
  • io/input,widget: [API] replace per-widget Focused with Source.Focused. Widgets have themselves as tags, by convention, and so it’s possible to replace the per-widget Focused methods with a general-purpose Source. Focused query. e59f91df
  • io/event: [API] rename InputOp to Op. dbc10056

Chris Waldon:

  • widget: [API] rename scrollbar update method to update. This matches the convention of other state update methods. While here, remove useless dimensions return. The update doesn’t draw anything, so there are no dimensions involved. e3241735
  • widget: [API] convert Editor to return one event at a time. This commit eliminates (*widget.Editor).Events() in favor of making (*widget.Editor).Update() return events as they are generated in response to input. This makes the behavior of the editor match the rest of the core widgets. Callers who previously invoked Events() can now achieve the same thing by using a loop like this: c645c2ec
  • widget: [API] simplify Selectable event processing. Now (*widget.Selectable).Update() returns whether the selection changed during event processing, rather than requiring a separate call to (*widget.Selectable).Events(). 297c0392

Changes by Author

Elias Naur:

  • io/pointer: clarify the documentation for Event.Position. 23b6f06e
  • .builds: fix apple builder. fc6e51de
  • widget/material: add missing Update calls. Without the updates, the switch and radiobutton would use stale state for layout. c458eb30
  • app: [Windows] fix restore size when leaving fullscreen. f7aa4b5c
  • app: don’t route internal wakeup events to the Router. 7d1ea022
  • flake.nix: upgrade to nixpkgs 23.11; upgrade to JDK 17. Apparently, newer Android SDKs now support Java versions newer than 8. Finally. a454d5fa
  • app: [Windows] tolerate gpu.ErrDeviceLost from Refresh. Fixes: #552 2128f7ad
  • io/key: delete Event.String. The String method doesn’t add anything in addition to the default Go formatting of the type. e19a2488
  • widget: remove assumption that Context.Queue is an interface. We’re about to make Context.Queue a concrete type, and this change replaces code that relies on Queue being an interface. 99399184
  • io/input: remove dependency on package gesture. This change is required to to replace event.Queue with a concrete input.Source. c319f3c2
  • widget/material: drop test dependency on package app. be86450e
  • widget: remove test dependency on package app. d7636ea2
  • io/input: deliver reset events lazily. Refactor delivery of reset events to be resolved and delivered as part of Source.Events. This is a preparation for changing event handling to be lazy. d2591267
  • io/input: remove pointerQueue.scratch optimization. We’re about the refactor this quite subtle code, and the optimization doesn’t seem to carry its weight in complexity. 3ba5fc55
  • io/input: merge event queues. Replace the per-event event queues with a single queue of events, each marked with the target tag. This change is a prerequisite for lazy event delivery. 651094d6
  • io/input: implement lazy event routing. This change defers event routing from the time the event is queued until the time Events is called. This allows a future change to execute commands immediately and to react to event order changes during a frame. 9dfada74
  • io/input: merge per-handler state. We’re about to need per-handler state related to neither pointer nor key input. This change merges the pointer and key handler state into one state struct, tracked in the Router. 4d8caba6
  • io/input: merge pointer and key filters. Refactor the pointer and key filter unions into the handler state struct. This is a preparation for replacing calls to filtersMatches with queries to the filter union. 67b58a60
  • all: replace InvalidateOp with InvalidateCmd command. Curiously, InvalidateCmd is probably the only command that is appropriate to call during layout. c515b780
  • all: use a single tag per widget for event handling. With the introduction of filters, it is now possible to have one tag per widget by convention. Note that gestures still have their own tags, for disambiguation. 75314fce
  • widget: show soft keyboard on focus. We’re about to replace the per-widget Focus methods with the client executing FocusCmd themselves. To ensure the soft keyboard is not forgotten, ask to show it automatically on focus. 6dcebf20
  • gesture: report one event at a time. Events are now delivered one at a time, and this change makes the corresponding change to gestures. 8e209fd2
  • io/input: permit FocusCmd to explicitly set the focus to any tag. If the client asks for the focus to be set to a tag, allow it. There is a check at the end of Router.Frame that clears the focus if the tag turns out to fail the requirements (visible and has asked for FocusEvents). 496fc3cc
  • io/input: tighten tests. Now that event delivery can be interleaved with commands, tests can be made more precise. 20c28ef2
  • text,widget: remove dead code and fields. 5fcfc40a
  • io/pointer: make Cancel non-zero. It’s semantically problematic that a zero Kind matches Cancel, and outweighs the downside of having to explicitly mention Cancel in filters. For example, GrabCmd was always deferred because the resulting Cancel events always match the processed filters. 33f9a850
  • io/input: test deferred behaviour of Router. 1fc646a8
  • io/input: discard pointer reset event if filter doesn’t match. New handlers receive reset events the first time Source.Event is called. However, in case the filter doesn’t match a reset event it shouldn’t be reported. f5aa7450
  • io/input: test Router.TextInputHint. 77ff2160
  • io/input: implement key.Filter.Name special case for matching every key. The empty key.Filter.Name now means matching every key name. This is a replacement for the previous special case where the top-level key.InputOp handler would get all unmatched events. c3f2abeb
  • app: update documentation. eae39d85
  • widget: update documentation. 5a843bee

Chris Waldon:

  • io/pointer: fix godoc reference to renamed type. d96c9547
  • widget/material: fix list scrollbar display. This commit fixes a visual misalignment in scrollbars resulting from subtle differences in the semantics of layout.Stack and layout.Background. layout.Stack will position expanded children according to their minimum constraint regardless of their returned size, whereas layout.Background uses their returned size. This means that layout.Expanded widgets returning zero dimensions are positioned correctly, but they break when converted to use layout.Background. 52987e53
  • app: fix automatic window decoration action processing. This commit adapts the use of the automatic window decorations to the event processing changes introduced in v0.4.0. You must update widget state before laying it out, not after. Doing so after (as this code used to do) results in discarding updates. ab021c45
  • io/input: fix docs for Router.Queue. The method no longer returns anything, and thus does not actually report whether any events matched a handler. 95ca7b5b

Dominik Honnef:

  • gesture: adjust ClickKind.String for ClickType -> ClickKind rename. e666ef35
  • widget: don’t refer to non-existent method Clickable.Clicks. 7ea432fa
  • gpu: remove unused cache parameters. 4eca2c7d
  • gpu: rename resourceCache to textureCache and use concrete key. The only remaining use of the cache is mapping handles to textures. Using a concrete type for the key avoids the allocation caused by convT. fe2a164d

James Stanley:

  • material: fix documentation of changing theme colours. adba14c0
  • material: fix documentation of creating an icon. 40706d37
  • material: fix documentation of using buttons. 7cfd226b

Egon Elbre:

  • op/paint: add nearest neighbor scaling. This adds support for nearest neighbor filtering, which can be useful in quite a few scenarios. 5fa94ff6
  • layout: add Background. It’s relatively common to create a widget and then add a background to it. Using layout.Stack causes bunch of heap allocs, which we would like to avoid whenever we can. f39245df

sewn:

  • widget/material: allow changing height & radius of progressbar. a8ec3968

Siva:

  • app: [macOS] activate app on ActionRaise if necessary. Calling window.Perform(system.ActionRaise) does not show the window on the top if the app is currently not active. This can happen for example if the app integrated with systray (https://pkg.go.dev/fyne.io/systray) where the menu item launches a window, the window is not showing at the top. It is fixed by activating the current app if necessary. 8097df99

Danny Wilkins:

  • internal/gl: fix startup crash on openbsd from libGLESv2 naming. 05d28ad7

gioui.org/x@v0.5.0

X was updated to use Gio v0.5.0, and Mearaj Bhagad fixed a bug in the nav drawer event processing.

API Change: Profiling and Eventx Packages Deleted

The profiling and eventx packages have both been deleted. profiling depended upon the now-gone core profile package, and eventx’s event manipulations are no longer necessary now that the event filters API is available.

API Change: Widget Update API

richtext, outlay, and colorpicker’s widgets have been updated to use the new style of Update(gtx) (event, bool) API, though this does not extend to all widgets in package component yet.

Breaking Changes by Author

Chris Waldon:

  • richtext: [API] fix widget update lifecycle. This commit updates richtext to have a split update+layout API like the rest of Gio, and fixes several issues and bugs in the design. Callers must now call (*richtext.InteractiveText).Update(gtx) to get events from interactive subregions, and such events now include granular hover/unhover events. 63894e7
  • eventx: [API] remove eventx package. This commit eliminates the eventx package. With the new Gio event filtering API, the spy and related helpers are no longer necessary. One can directly intercept the events destined for a child widget when desired. 96e8290
  • profiling: [API] remove profiling package. Gio upstream no longer supports this strategy for gathering graphics profiling information, so this package is no longer useful. 2da524a
  • outlay: update remaining layouts to new gio API. This commit updates all remaining outlay layouts to use the new API and splits the Grid type’s Update and Layout operations to better match upstream widgets. 01d79de
  • richtext: [API] update to new upstream APIs. This commit makes package richtext compatible with the new usptream event filtering while also updating the richtext API with separate Update and Layout events. Like all Update() methods, the text ones must be invoked in a loop until they indicate no remaining events for the current frame. 2016b35
  • colorpicker: [API] update to event filtering API. This commit updates the colorpicker to process events using the new event filters API and also renames colorpicker.State.Layout to colorpicker.State.Update. This method now returns whether the colorpicker’s color may have changed as a result of user input. The Changed method has been deleted as unnecessary. Note that previously, Changed would return true if the color was changed by a setter method, but now Update only returns true if event processing changed the color. 576bf83

Changes by Author

Chris Waldon:

  • go.*: update to gio v0.4.1. 23aab4c
  • outlay: fix invalidation api. 09d3751
  • styledtext: fix tests. cde3347
  • debug: [broken] update to event filter API. This commit updates the constraint debugger to use event filters. Sadly, this appears to tickle a bug in the new event delivery service that prevents the debugger from ever gaining keyboard focus. See: 0561bd2
  • component: convert to event filters API. 794c907
  • go.*: update to upstream event filters branch. a9f45ca
  • go.*: tidy modules. e88b6b2
  • go.*: update to gio v0.5.0. a79b18f

Mearaj Bhagad:

gioui.org/example@v0.5.0

Example was updated to be API-compatible with Gio v0.5.0 and Gio-x v0.5.0.

Changes by Author

Chris Waldon:

  • go.*: update to gio v0.4.1. fe672ba
  • go.*: update gio for wayland window decoration fix. 91a66c8
  • go.*: update gio-x for nav bar bugfix. c5bf31e
  • go.*,all: update to gio and gio-x v0.5.0. 9d695ee

giouiorg

Egon updated the website for correctness and added a showcase application. Thanks Egon!

Changes by Author

Egon Elbre:

  • doc/{learn,architecture}: fix code for gioui.org@v0.4.1. 69f590c
  • go.mod: update gogio and example dependency. 3bfa764
  • doc/showcase: add Cryptopower. 49d5811

Chris Waldon:

  • content: fix gotraceui image extension. 8de0106

End

Thanks for reading!

Chris Waldon