8.8 KiB
UPDATE 18 March 2014: this document is out of date and could use some rewriting
ui library implementation information
All platform-specific functionality is isolated in a mega-type sysData
that stores OS toolkit handles and provides methods that do the work for the frontend API. The file sysdata.go
defines a type cSysData
that contains everything all platforms have in common and dummy definitions of the sysData
functions that panic. The platform-specific sysData
embeds cSysData
.
The key sysData
function is sysData.make()
. It takes two arguments: the initial text of the control (if any), and a pointer to a sysData
that represents the window that holds the control. If this pointer is nil
, we are creating a window instead, so any window-specific actions are taken here.
cSysData
contains the control type as a number, an event
channel where the object's standard event (close button for Window
, click for Button
, etc.) is stored, and a resize()
function variable that is called to resize child controls on window resize.
Controls need only two functions: a make()
function that builds the control, and a setRect()
function that positions the control in its parent window. make()
takes a single argument: the parent window. This window's sysData
field gets passed to the control's sysdata.make()
to indicate that it is a child control.
To keep things thread safe, all UI functions should be run on a separate thread whose OS thread is locked. This thread is goroutine ui()
, presently started by init()
(but it may be started by a dedicated function later). This goroutine is defined per platform, but takes the general form of
func ui(initErrors chan error) {
runtime.LockOSThread()
initErrors <- doPlatformInit() // inform init() of success or failure
for {
select {
case message <- uitask:
do(message)
default:
platformEventLoopIteration()
}
}
}
uitask
is a channel that transmits messages. These messages indicate platform-specific functions to call and their arguments.
All sysData
methods except sysData.setRect()
and sysData.preferredSize()
dispatch through uitask
. Control resizing is handled within the UI goroutine itself, so sysData.setRect()
(and thus sysData.resize()
) call resizing functions directly. (The GTK+ backend broke spectacularly otherwise.) sysData.preferredSize()
is called by the resizing functions, so thus must also run on directly.
Windows
On Windows, all controls are windows, window classes are used to define their type, and messages are used to perform actions on windows and dispatch(different word? TODO) events. The data that we need to store, then, is the class name, initial styles, and combobox/listbox messages.
For Window
s, a new window class is created for each window that you open. This window class is only different by its message handling function, or window procedure/WndProc. The WndProc is generated as a closure, so that it can safely absorb the window's sysData
(so we don't need t look it up manually). This is all in stdwndclass_windows.go
.
For controls, Windows uses an ID number to identify controls to the parent window, rather than passing around the window handles directly. The sysData
for a window takes care of all this.
uitask
transmits structures of type uimsg
, which contain three things:
- A `syscall.LazyProc` to call
- The parameters, as a `[]uintptr` (just like `syscall.LazyProc.Call()`)
- A channel to transmit `system.LazyProc.Call()`'s return values on
Unix (except Mac OS X)
GTK+ is strange: there are constructor functions that return GtkWidget *
, but anything that actually accesses a control requires the GtkWidget *
to be cast to the appropriate control type. Fortunately, we can do this at time of call and just store GtkWidget *
s for everything. And as most of these control methods take the same form, we can just store a list of functions to call for each control type.
uitask
is a channel that takes func()
literals. These are closures generated by the sysData
functions that contain the GTK+ calls and a channel send for return values (like with Windows above). If no return value is needed, the channel send just sends a struct{}
. The UI goroutine merely calls these functions.
As the GTK+ main loop system does not quite run in a sane way (it allows recursion, and the gtk_main_loop_iteration_do
function only onperates on the innermost call), we cannot use the for
/select
template for ui()
. Fortunately, GDK provides gdk_threads_add_idle(), which allows us to run, and optionally (and importantly) run only once, a function on the gtk_main()
thread when not processing events. We use this, combined with a goroutine for dispatching, to handle uitask
requests. See our_idle_callback
in callbacks_unix.go for details.
GTK+ layout managers are not used since the UI library's layout managers are coded in a portable way. (A GtkLayout
in a GtkScrolledWindow
is used instead. This is done instead of just a GtkFixed
so that a window can be resized smaller than the size requests of its contents.) This isn't ideal, but it works for now.
All event handlers take the sysData
as their user data parameter; this means all the event-handling code is stored in static functions in callbacks_unix.go. (Early versions of the package generated signal handlers for each control on the fly, but this needed to be changed to accommodoate Area, which not only needs the sysData
but also needs to connect to a subwidget of a subwidget (specifically the subwidget of the GtkViewport
of a GtkScrolledWindow
); the current setup also avoids creating closures for each and every Window and Button created, and also means we can stop having to shove those callbacks in an ever-growing slice to prevent them from being garbage collected.) Should the widget actually be a child widget of a GtkScrolledWindow
, the child
function and childsigs
signal list are used to assign signals as well.
The only major snag with the GTK+ implementation is the implementation of Listbox
; see listbox_unix.go
for details.
Mac OS X
The Mac OS X backend uses Cocoa. As Cocoa uses Objective-C, we work through the Objective-C runtime, whose various methods are called directly from the Go code. The biggest problem is that the message dispatch function, objc_msgSend
, takes a variable number of arguments, and cgo can't handle this. Consequently, I have to use a bunch of wrapper functions, made in objc_darwin.h
and bleh_darwin.m
, instead. bleh_darwin.m
implements a bunch of edge cases where limitations of cgo or problems with the documentation or implementation do not allow otherwise (hence the filename).
Before the Mac OS X backend was written, you put your code in main.main()
. Unfortunately, Cocoa requires that you place its event loop on the very first OS thread created, called the "main thread", and provides no way to move things to another thread. (Windows and GTK+ don't seem to care which thread you use so long as you use one thread.) This is why you have to put your code in another function and call ui.Go()
to use the library.
Otherwise, the sysData
for Cocoa just uses a bunch of functions that call the necessary Objective-C functions ("selectors") to create and modify controls.
Event handling is done with a delegate class in delegate_darwin.go
. This class is created at runtime (a feature that seems to have been introduced in Mac OS X 10.5) and a single instance is shared by everything. This instance is also responsible for uitask
dispatch: Cocoa provides facilities for running an object's selector on the main thread. uitask
communicates func()
literals, just like the GTK+ backend.
Cocoa coordinate systems have (0,0) at the bottom-left corner of each rectangle (be it the screen, a window, or a control) and has Y go up as it increases; everything else places (0,0) at the top-left and has Y go down as it increases. The various setSize()
methods have thus been adorned with the window's content area's height to do the necessary fixup. This was done to avoid polling the window's height many times each time a window is resized.
The biggest problem with Cocoa is that you were never intended to use it directly for GUI work: you were intended to use Interface Builder for everything. For the most part, we can get by, as there aren't too many functions and most things are documented well enough. Listboxes and other cases of NSTableView, on the other hand, are not. Read listbox_darwin.go
for more details.
Other limitations caused by Cocoa are described in their respective soruce files.