After we wrapped up mouse input in the previous article, it's time to breathe a sigh of relief. This article should be much lighter, although it is a bit longer than usual. We can put our work with mouse input and rendering all together and finally create buttons and labels, two classes of elements. With these, our UI library will be getting much closer to a workable state.
A message class will typically need 3 things: a creation function, a message handler function, and a structure to store its state. Labels and buttons will demonstrate this very well. Let's start with the button.
typedef struct Button {
Element e; // The common element header.
char *text; // The button's label.
size_t textBytes;
} Button;
Button *ButtonCreate(Element *parent, uint32_t flags, /* by convention, all element creation functions start with these 2 parameters */
const char *text /* the button's label */, ptrdiff_t textBytes /* -1 indicates a zero terminated string */) {
// Create the button element.
Button *button = (Button *) ElementCreate(sizeof(Button), parent, flags, _ButtonMessage);
// Copy the input text into the button's storage.
// This way the user of the library doesn't have to keep their copy around.
StringCopy(&button->text, &button->textBytes, text, textBytes);
// Return a pointer to the button.
return button;
}
The message handler will need to respond to 2 messages. First is the MSG_PAINT
message to actually draw the button. This will first decide the colors for the button (depending on whether it needs to appear pressed), and then draw a rectangle of its bounds and the label string. The second message is MSG_UPDATE
, where the button will ask to be repainted at the next _Update
.
int _ButtonMessage(Element *element, Message message, int di, void *dp) {
// Cast the element to a Button.
Button *button = (Button *) element;
if (message == MSG_PAINT) {
// Get the Painter structure passed via dp.
Painter *painter = (Painter *) dp;
// The button should appear pressed if the mouse cursor is hovering over it and is pressing down on it.
// That is, precisely the conditions needed to get MSG_CLICKED to fire when the mouse button is released.
bool pressed = element->window->pressed == element && element->window->hovered == element;
// Pick the colors for the button.
uint32_t c = 0xFFFFFF;
uint32_t c1 = pressed ? 0xFFFFFF : 0x000000, c2 = pressed ? 0x000000 : c;
// Draw a rectangle in the button's bounds, and draw the label text centered in the bounds.
DrawRectangle(painter, element->bounds, c2, c1);
DrawString(painter, element->bounds, button->text, button->textBytes, c1, true);
} else if (message == MSG_UPDATE) {
// Queue the entire button to be repainted.
ElementRepaint(element, NULL);
}
return 0;
}
How is the button used? Let's have a look at a simple example of possible usage code. First the user creates the button.
Button *myButton = ButtonCreate(parent, 0 /* no flags */, "Click here", -1);
Then, we set the messageUser
callback on the element.
myButton->e.messageUser = MyButtonMessage;
And then we implement the message handler.
int MyButtonMessage(Element *element, Message message, int di, void *dp) {
if (message == MSG_CLICKED) {
// Do something here!
}
return 0; // Let the messageClass handler receive the message next.
}
It's a little verbose, yes, but there are many tricks to slim this code down. We'll do something a little hacky with C macros later on in the tutorial. But there are better mechanics to allow responses to button actions; it's just that they're out of the scope for this tutorial. This method with messageUser
callback is nice though because it will extend easily to more complicated elements, where there are many messages the user (user meaning user of the library) will want to inspect or override.
Now, on to labels! Much like buttons, we need a structure, creation function and message handler.
typedef struct Label {
Element e;
char *text;
size_t textBytes;
} Label;
int _LabelMessage(Element *element, Message message, int di, void *dp) {
Label *label = (Label *) element;
if (message == MSG_PAINT) {
DrawString((Painter *) dp, element->bounds, label->text, label->textBytes, 0x000000, false);
}
return 0;
}
Label *LabelCreate(Element *parent, uint32_t flags, const char *text, ptrdiff_t textBytes) {
Label *label = (Label *) ElementCreate(sizeof(Label), parent, flags, _LabelMessage);
StringCopy(&label->text, &label->textBytes, text, textBytes);
return label;
}
There really aren't any surprises here. Our implementation of a label is nearly identical to that of a button, but with simpler painting, and we don't process the MSG_UPDATE
message. You may be thinking at this point we might want to move the text management stuff into the common Element
header, but I don't think this is necessary. Buttons and labels are really the only places where this all happens. Possibly textboxes too, but even they are often too specialized. Other elements you come across like panels, sliders and list views simply don't have a single text string they're associated with. That said, you could add a description string to the common element header that screen readers could use, but accessibility concerns are out of scope for this toy UI library.
There's a few extra little features I want to add to our elements. First, I want it to be possible to make a label center its text in its bounds, rather than left align it. We will introduce a flag. Remember flags? I don't think we've actually defined any yet. Well, now's a good time as ever. Flags are passed into the element creation function, and then saved in the common element header by ElementCreate
. They're easily accessible from the message handlers. The flags
field is a uint32_t
. The low 16 bits are defined by the element class, and the high 16 bits are common to all elements. We'll be defining some common flags in the next article. So, let's define a LABEL_CENTER
flag and use it when painting the label.
#define LABEL_CENTER (1 << 0)
int _LabelMessage(Element *element, Message message, int di, void *dp) {
// ...
DrawString((Painter *) dp, element->bounds, label->text, label->textBytes, 0x000000,
element->flags & LABEL_CENTER /* NEW */);
// ...
}
The next feature I want to add is the ability to change the label's contents after creating it, so we introduce LabelSetContent
. This is a very easy function to write, since we've already got the StringCopy
helper.
void LabelSetContent(Label *label, const char *text, ptrdiff_t textBytes) {
StringCopy(&label->text, &label->textBytes, text, textBytes);
}
You might want to put a call to ElementRepaint(&label->e, NULL);
in this function. However, I've chosen to leave it up to the user of the library to remember to make the repaint call. We similarly didn't make ElementMove
call into ElementRepaint
, requiring the user to call it after triggering layouts. I don't think this is the right decision, but this is a just a toy UI library, so it's nice to play around with different API designs.
Finally, I want a method of changing the color of buttons. Instead of putting a color field in the button header, I want to demonstrate a trick we can do with message to allow the button to query it's own color 'on demand'. We first define a new message, MSG_BUTTON_GET_COLOR
. Then in the button's message handler, we write:
// Pick the colors for the button.
uint32_t c = 0xFFFFFF;
ElementMessage(element, MSG_BUTTON_GET_COLOR, 0, &c); // NEW!
uint32_t c1 = pressed ? 0xFFFFFF : 0x000000, c2 = pressed ? 0x000000 : c;
We don't implement the message in the messageClass
of the button. However, if a user of a button wants to change it's color, in their messageUser
handler they'd be able to write something like:
if (message == MSG_BUTTON_GET_COLOR) {
*(uint32_t *) dp = 0x00FF00; // Green.
}
I don't think this is a particularly good way to do styling, but it demonstrates nicely a message handler trick of an element sending a message to itself. Proper styling is out of the scope of this tutorial, as it is a huge rabbit hole. The complexity comes from trying to get the functionality of both CSS and SVGs at the same time (and having to provide the tooling to author styles).
Finally, we're done. Let take a little look at the test usage code for this article.
Here's the main
function.
Button *myButton;
Label *myLabel;
int counter = 0;
int main() {
// Initialise the UI libary and create a window.
Initialise();
Window *window = WindowCreate("Hello, world", 300, 200);
// Add a custom element to the window to manage laying out the interface.
Element *layoutElement = ElementCreate(sizeof(Element), &window->e, 0, LayoutElementMessage);
// Add a button and label to the layout element.
myButton = ButtonCreate(layoutElement, 0, "Increment counter", -1);
myButton->e.messageUser = MyButtonMessage;
myLabel = LabelCreate(layoutElement, LABEL_CENTER, NULL, 0);
// Update the label to match the current value of the counter.
UpdateLabel();
// Process input events from the operating system.
return MessageLoop();
}
The LayoutElementMessage
function draws a pink background and moves the button and label in response to the MSG_LAYOUT
message. It's a fairly standard affair. Let's look at the UpdateLabel
function.
void UpdateLabel() {
// Set the label's contents.
char buffer[50];
snprintf(buffer, sizeof(buffer), "Click count: %d", counter);
LabelSetContent(myLabel, buffer, -1);
// Recall our discussion above. We could easily change our API to make this call unneeded,
// having moved the responsible of calling it into LabelSetContent.
// But I think it's better for the tutorial to be more explicit about what's happening.
ElementRepaint(&myLabel->e, NULL);
}
Finally, let's look at the message handler for the button. This is responsible for updating the counter when clicked. It therefore also needs to call UpdateLabel
when clicked. As noted above, this mechanism for responding to button presses is a little verbose, and doesn't mesh nicely when you try to use the same action with a keyboard shortcut and menu item. But improving this is out of scope for the tutorial.
int MyButtonMessage(Element *element, Message message, int di, void *dp) {
if (message == MSG_CLICKED) {
counter++; // Increment the counter.
UpdateLabel(); // Update the label's contents.
}
return 0;
}
And that's it for buttons and labels!