Skip to content
Randall C. O'Reilly edited this page Jan 25, 2024 · 1 revision

Events

The oswin OS-specific window system drivers send Mouse, Key, Window etc events to the gi.Window, which then sends those events on to the specific widgets.

Each widget registers to receive specific types of events from the window using the ConnectEvent method, and typically includes a function right there that processes the event. e.g., here is the event processing call for a Button MouseEvent:

// MouseEvents handles button MouseEvent
func (bb *ButtonBase) MouseEvent() {
	bb.ConnectEvent(oswin.MouseEvent, RegPri, func(recv, send ki.Ki, sig int64, d interface{}) {
		me := d.(*mouse.Event)
		bw := recv.(ButtonWidget)
		bbb := bw.ButtonAsBase()
		if me.Button == mouse.Left {
			switch me.Action {
			case mouse.DoubleClick: // we just count as a regular click
				fallthrough
			case mouse.Press:
				me.SetProcessed()
				bbb.ButtonPressed()
			case mouse.Release:
				me.SetProcessed()
				bw.ButtonRelease()
			}
		}
	})
}

The ConnectEvents2D method is the standard Node2D interface method that is called to register these event processing methods, during the Render2D call, only if the node is actually visible. It is good practice to create separate type-specific methods for the different kinds of event processing (or multiple if they always should go together) so that derived types can re-use those event processors, while also adding their own as needed. Here's what that looks like on ButtonBase:

func (bb *ButtonBase) ButtonEvents() {
	bb.HoverTooltipEvent()
	bb.MouseEvent()
	bb.MouseFocusEvent()
	bb.KeyChordEvent()
}

func (bb *ButtonBase) ConnectEvents2D() {
	bb.ButtonEvents()
}

func (bb *ButtonBase) Render2D() {
	if bb.FullReRenderIfNeeded() {
		return
	}
	if bb.PushBounds() {
		bb.This().(Node2D).ConnectEvents2D()
		...
	} else {
		bb.DisconnectAllEvents(RegPri)
	}
}

Event Loops

From the end-user app perspective, everything is asynchronously event-driven through the event processing functions as shown above. E.g., someone hits a button and that ends up calling relevant code..

Under the hood, the main thread is running an event loop that processes the oswin OS-driven events (you can also run code on this main thread using oswin.TheApp.RunOnMain()), and then each window is running its own separate goroutine for its own event loop, where it reads from its own event channel and then sends those out to the widgets.

The widget event calls are made in the same goroutine as the window event loop, so they are blocking and sequential on that thread of processing. Thus, any more significant processing triggered by an event should generally call go at some point to fork off into a separate goroutine. Once that occurs, then event processing should proceed in parallel through the go scheduler continuing to run the different event processing loops. Also you definitely do not want to run anything long and blocking on the oswin.TheApp.RunOnMain() as that will block all gui events.

Another alternative for longer-running functions is to periodically call PollEvents() on the window, which will process any currently-waiting events but will not wait for new ones. This first calls PollEvents for the system events, and then handles any events at the gi.Window level.

See Updating for important notes about coordinating updates from a separate go routine.

When a main program starts up, the last call is typically win.StartEventLoop() which then runs until the window is closed, at which point execution will return to the next line after that call. All other windows should be opened using win.GoStartEventLoop() which just runs the loop using go in a separate routine.

If multiple different windows can be opened and execution only terminates after the last one is closed, then start all event loops using win.GoStartEventLoop() to run on separate goroutines, and use this call:

gi.WinWait.Wait()

WinWait is a sync.WaitGroup that gets added to with the GoStartEventLoop call.

Clone this wiki locally