Using the Lua Programming Language to Create a Graphical User Interface

Most toolkits for graphical user interfaces organize the elements of the user interface, typically called "widgets", in a hierarchy: Starting with some container like a dialog or window at the top, the elements like push buttons, labels, or layout managers are added. To add new elements a set of C functions is provided which are called with a handle to the parent widget, the class (or type) of widget that is to be added plus any further arguments like e.g. a label string in the case of a push button. While it is relatively easy to expose the widget creation and managing functions to Lua, the resulting Lua program will still closely resemble the same functionality coded in C. So the aim is to use Lua's table constructors to define a GUI instead of calling the widget creation routines. The following small program, using the OpenMotif toolkit, as all remaining examples, serves as an illustration:
#include <stdio.h>

#include <Xm/PushB.h>

void button_pushed(Widget, XtPointer, XtPointer);

int
main(int argc, char *argv[])
{
        Widget toplevel, button;
        XtAppContext app;
        XmString label;
 
        XtSetLanguageProc(NULL, NULL, NULL);

        toplevel = XtVaAppInitialize(&app, "Hello", NULL, 0, &argc, argv,
            NULL, NULL);

        label = XmStringCreateLocalized("Push here to say hello");
        button = XtVaCreateManagedWidget("pushme", xmPushButtonWidgetClass,
            toplevel,XmNlabelString, label, NULL);
        XmStringFree(label);
        XtAddCallback(button, XmNactivateCallback, button_pushed, NULL);

        XtRealizeWidget(toplevel);
        XtAppMainLoop(app);
}

void
button_pushed(Widget widget, XtPointer client_data, XtPointer call_data)
{
        printf("Hello yourself!\n");
}
When a mere 1:1 mapping of the C functions to Lua is provided, the resulting Lua program will look like this:
require "motif"
	
function button_pushed(button)
       print("Hello yourself!")
end

XtSetLanguageProc(nil, nil, nil)

toplevel, app = XtAppInitialize("XLua")
label = XmStringCreateLocalized("Push here to say hello")
button = XtVaCreateManagedWidget("pushme", xmPushButtonWidgetClass, toplevel,
    XmNlabelString, label)
XmStringFree(label)
XtAddCallback(button, XmNactivateCallback, button_pushed)
XtRealizeWidget(toplevel)

XtAppMainLoop(app)
This code still looks like the C program and it has no real advantage over the original version. The only difference is that Lua is being used instead of C. So the real goal is to use Lua's table constructor syntax to declare the hierarchy of user interface elements. The small hello word example should rather look like the following example:
require "motif"

button = PushButton {
	labelString = "Push here to say hello",
	activateCallback = function ()
		print("Hello yourself!")
	end
}

SetLanguageProc(nil, nil, nil)
toplevel, app = Initialize("XLua")

Realize(toplevel, button)
MainLoop(app)
Lua's table constructor syntax is used to define a (very simple) widget hierarchy, whith just one push button that has two arguments, a label string and a callback function that is called when the button is pressed. One thing to note is that Lua will call a global function "PushButton" when creating the "button" table. This function call can be used to actually create the widget. A first approach could be to write "constructor" functions for all widgets that create the widget when called; thus providing a direct mapping from the table construction process to the widget creation process. But this will not work. Consider a true hierarchy of elements like in the following, longer, example:
require "motif"

gui = Form {
	fractionBase = 3,
	Frame {
		topAttachment = ATTACH_FORM,
		rightAttachment = ATTACH_FORM,
		bottomAttachment = ATTACH_POSITION,
		leftAttachment = ATTACH_FORM,

		bottomPosition = 1,

		LabelGadget {
			labelString = "Motif Buttons in Lua",
			childType = FRAME_TITLE_CHILD
		},
		RowColumn {
			PushButton {
				labelString = "Button one",
				activateCallback = function()
					print("Button one was pressed")
				end
			},
			PushButton {
				labelString = "Button two",
				activateCallback = function()
					print("Button two was pressed")
				end
			},
		}
	},
	TabStack {
		topAttachment = ATTACH_POSITION,
		rightAttachment = ATTACH_FORM,
		bottomAttachment = ATTACH_FORM,
		leftAttachment = ATTACH_FORM,

		topPosition = 2,

		tabSide = TABS_ON_BOTTOM,
		tabStyle = TABS_ROUNDED,

		tabCornerPercent = 100,
		tabMarginHeight = 64,
		tabMarginWidth = 64,

		products = LabelGadget {
				labelString = "frame title",
		},
		groups = Frame {
			LabelGadget {
				labelString = "frame title",
				childType = FRAME_TITLE_CHILD,
			},
			rc = RowColumn {
				PushButton {
					labelString = "pushme",
					activateCallback = function (w)
						print(tf:GetString())
						tfield = w:Parent().t
						print(tfield:GetString())
					end
				},
				t = TextField {
					activateCallback = function (w)
						print(w:GetString())
					end,
					valueChangedCallback = function (w)
						print(w:GetString())
					end,
					focusCallback = function (w)
						print("Button down " ..
						    w:GetString())
					end
				}
			}
		},
	}

}

tf = gui[3].groups.rc.t

SetLanguageProc(nil, nil, nil)
toplevel, app = Initialize("XPoS")

Realize(toplevel, gui)

MainLoop(app)
The problem is that while the widget hierarchy must be created with the top element first (the Form) and then adding the child elements in descending order, Lua constructs the table from the inside out, starting with the leaf nodes. In the example above, Lua would first create the PushButton, then add it to the RowColumn, then add the RowColumn to the Frame etc. Lua creates leaf widgets when the parent widget has not yet been created. A working approach is to just "take note" in the constructor functions which widget has to be created, but to delay the actual widget creation until the whole table tree has been constructed by Lua. Once this is done, a function that actually creates the widgets, starting with the top element, can be called. The function "Realize(toplevel, gui)" does exactly that. So what do constructor functions look like? They are called by Lua with the corresponding table object on the top of the stack, so a field, __widgetClass, can be set to hold the widgetClass as lightuserdata which will later be used to create the widget.
static int
PushButton(lua_State *L)
{
	lua_pushlightuserdata(L, xmPushButtonWidgetClass);
	lua_setfield(L, -2, "__widgetClass");
	return 1;
}
The "Realize()" function will create the widget hierarchy in the following steps:
  • Loop over the table to collect arguments to the widget to be created
  • Create the widget with the arguments
  • Do a second loop over the table to create child widgets and to add callbacks to the widget
So why don't we just create the widgets and add the arguments at the end, doing e.g. a XtSetArgs() call, saving us one loop over the table? The reason is that some widgets don't allow some of their attributes to be changed after widget creation. e.g. the scrollingPolicy of a ScrolledWindow can not be changed after the ScrolledWindow has been created.
static Widget
Realize(lua_State *L, int parentObj, Widget parent, const char *name)
{
	Arg args[MAXARGS];
	WidgetClass class;
	Widget widget;
	XmString s;
	struct cb_data *cbd;
	char nam[64], *nm;
	int n, narg, t;
	const char *cb;
	const char *func;

	widget = NULL;
	narg = 0;

	lua_pushstring(L, "__widgetClass");
	lua_rawget(L, -2);
	class = (WidgetClass)lua_topointer(L, -1);
	lua_pop(L, 1);

	if (class != NULL) {
		t = lua_gettop(L);
		lua_pushnil(L);
		while (lua_next(L, t) != 0) {
			switch (lua_type(L, -2)) {
			case LUA_TSTRING:
				strlcpy(nam, lua_tostring(L, -2), sizeof nam);
				break;
			case LUA_TNUMBER:
				snprintf(nam, sizeof n, "%.f", lua_tonumber(L, -2));
				break;
			default:
				strlcpy(nam, name, sizeof nam);
			}

			switch (lua_type(L, -1)) {
			case LUA_TSTRING:
				XtSetArg(args[narg], nm, lua_tostring(L, -1));
				narg++;
				break;
			case LUA_TNUMBER:
				nm = strdup(nam);
				XtSetArg(args[narg], nm, (XtArgVal)
				    (int)lua_tonumber(L, -1));
				narg++;
				break;
			case LUA_TBOOLEAN:
				nm = strdup(nam);
				XtSetArg(args[narg], nm, (XtArgVal)
				    (int)lua_toboolean(L, -1));
				narg++;
				break;
			}
			lua_pop(L, 1);
		}
	}

	if (class == xmDialogShellWidgetClass)
		widget = XtCreatePopupShell(name, class, parent, args, narg);
	else if (class != NULL)
		widget = XtCreateManagedWidget(name, class, parent, args, narg);
	if (widget != NULL) {
		lua_pushlightuserdata(L, widget);
		lua_setfield(L, -2, "__widget");
		luaL_getmetatable(L, WIDGET_METATABLE);
		lua_setmetatable(L, -2);
	}

	t = lua_gettop(L);
	lua_pushnil(L);
	while (lua_next(L, t) != 0) {
		switch (lua_type(L, -2)) {
		case LUA_TSTRING:
			strlcpy(nam, lua_tostring(L, -2), sizeof nam);
			break;
		case LUA_TNUMBER:
			snprintf(nam, sizeof n, "%.f", lua_tonumber(L, -2));
			break;
		default:
			strlcpy(nam, name, sizeof nam);
		}

		switch (lua_type(L, -1)) {
		case LUA_TTABLE:
			if (widget == NULL)
				CreateWidgetHierarchy(L, t, parent, nam);
			else
				CreateWidgetHierarchy(L, t, widget, nam);
			break;
		case LUA_TFUNCTION:
			if (widget == NULL)
				break;

			cbd = malloc(sizeof(struct cb_data));
			if (cbd == NULL)
				luaL_error(L, "memory error");
			cbd->L = L;
			lua_pushvalue(L, -1);
			cbd->ref = luaL_ref(L, LUA_REGISTRYINDEX);
			lua_pushvalue(L, t);
			cbd->obj = luaL_ref(L, LUA_REGISTRYINDEX);
			XtAddCallback(widget, nam, lm_Callback, cbd);
			XtAddCallback(widget, XmNdestroyCallback,
			    lm_DestroyCallback, cbd);
			break;
		}
		lua_pop(L, 1);
	}
	for (n = 0; n < narg; n++)
		free(args[n].name);
	if (parentObj > 0) {
		lua_pushvalue(L, parentObj);
		lua_setfield(L, -2, "__parent");
	}
	return widget;
}

Comments

Full source code on github.

https://github.com/mbalmer/luamotif