In this article we're going to add messaging infrastructure to our toy UI library.

Now that we've got our hierarchy of elements set up, we want make it so that elements can respond to different requests that might occur in a UI in a generic way. For example, if an element is left mouse clicked on, then we'll send the element a 'left mouse clicked' message. The specific element is then free to perform whatever action it wants. Or, in the case of something static like a label or image display, it can ignore the message. There'll also be other messages like the 'paint' message which asks an element to draw itself onto the window's bitmap, or the 'destroy' message which tells an element its time to free its resources as it's about to removed from the UI hierarchy and deallocated.

An element will receive messages by means of a function pointer callback, placed in the common Element header.

typedef int (*MessageHandler)(struct Element *element, Message message, int di, void *dp);

typedef struct Element {
	// ...
	MessageHandler messageHandler;
	// ...
} Element;

Let's take a look at what each of the parameters passed to the callback mean.

element is the specific element that's receiving the message. This makes it possible for different elements to use the same message handler function. This is good because there will usually be more than one of a specific type of element. For example, an interface will usually contain lots of different labels, all of which could share the same message handler. When responding to say, the paint message, the message handler for the label would presumably cast the element to a Label * and draw the text in a text field, or similar.

The message field is the identifier of the specific message; Message is a enum. For now, we haven't got any messages, but let's put MSG_USER in the enum. This will always be the last message in the enum, and indicates the message identifiers that the user of the library are free to define themselves: MSG_USER + 0, MSG_USER + 1, etc.

typedef enum Message {
	MSG_USER,
} Message;

The di and dp fields of the callback are used to pass the parameters of the message. I have found that it is typical to want to send either an integer, pointer or both in a message, which is how I settled on this configuration. You could remove di and just store the data for all messages within the dp pointer, but that would be less easy to use. The return value int of the callback is again defined by the message type, although 0 should be returned by the element if it doesn't recognize or respond to a particular message. In this way, an empty message handler looks like this:

int EmptyMessageHandler(Element *element, Message message, int di, void *dp) {
	return 0;
}

Since every element needs a message handler, we will add it as a parameter to ElementCreate, which will just set the corresponding field in the structure.

Element *ElementCreate(size_t bytes, Element *parent, uint32_t flags, MessageHandler messageHandler)  {
	Element *element = (Element *) calloc(1, bytes);
	element->flags = flags;
	element->messageHandler = messageHandler;
	// ...
	return element;
}

We now need to create a message handler for the Window element. Since we haven't defined any messages to use yet, this will be an empty message handler.

int _WindowMessage(Element *element, Message message, int di, void *dp) {
	return 0;
}

And when we create the window element, we pass this function as a function pointer to ElementCreate.

Window *window = (Window *) ElementCreate(sizeof(Window), NULL, 0, _WindowMessage);

All good? Well, there's actually a little problem with this current setup. As we noted above when looking at the case of labels, every label element has to do the same thing: when painting, it draws the text stored in one of its fields. Therefore all labels can share the same message handler. However let's consider the case of buttons. Every button will indeed perform the same task when painting; it'll draw a box and then some text on top taken from some field. But when a button is clicked its behaviour will differ for each button! So it seems there are two layers of message handling. The first layer is specific to the 'class' (type) of the element, and the second layer is specific to the individual element (or just some smaller subset). We want an element to respond to messages by default in the generic way as defined by the 'class', but we want it to be possible for the 'user' (user as in the user of the library) to potentially change the response to any given message. Thus, we split the messageHandler field in the Element into two: messageClass and messageUser.

typedef struct Element {
	// ...
	MessageHandler messageClass, messageUser;
} Element;

We then provide the following wrapper function for sending messages to an element. This first tries sending the message to the messageUser handler. If this handler returns 0, then it proceeds to call into the messageClass handler, to get the default behaviour. If the messageUser field is unset, then it always goes to the messageClass handler.

int ElementMessage(Element *element, Message message, int di, void *dp) {
	if (element->messageUser) {
		int result = element->messageUser(element, message, di, dp);

		if (result) {
			return result;
		} else {
			// Keep going!
		}
	}

	if (element->messageClass) {
		return element->messageClass(element, message, di, dp);
	} else {
		return 0;
	}
}

When we want to send a message to an element, we call this function, and it automatically takes care of the neccessary logic. Awesome~!

This setup also allows us to perform a trick of where an element can send messages to itself to ask the user (as in the user of the library) for additional settings about the element, or the notify the user of element-specific events. But we won't get to that in this tutorial.

That's all for this article. In the test usage code we create two elements and send them some messages. If you're feeling a bit shaky about all this, step through the code to make sure you understand why it's doing what it's doing.

part4.c

Part 3: Element hierarchy.

Part 5: Layouting infrastructure.