We start by adding a list of elements that need syncing. We'll use stb_ds.h to store this as a dynamic array.

Element **reactElements; // Global variable.

In DocumentFree and at the start of Populate we will empty this array and free it.

void DocumentFree() {
	for (int i = 0; i < hmlen(objects); i++) {
		ObjectFree(objects[i].value);
	}

	hmfree(objects);
	arrfree(reactElements); // NEW!
}

void Populate() {
	arrfree(reactElements); // NEW!
	// ...
}

Next we're going to create a new function ReactUpdate which will send out the message #define MSG_PROPERTY_CHANGED (MSG_USER + 1) to all the elements of this array that match a specific property key. This is how we're going to do synchronisation: when a specific property is modified, this function is called with the key, and then all the elements that receive the MSG_PROPERTY_CHANGED message are responsible for updating themselves.

void ReactUpdate(Element *exclude /* a specific element to exclude from updating, or NULL */, 
		const char *key   /* the property key to match, or NULL to match all elements */) {
	// For every element in the reactElements arrays...
	for (int i = 0; i < arrlen(reactElements); i++) {
		// Get the data pointer from the cp field.
		ReactData *data = (ReactData *) reactElements[i]->cp;

		// If this is the element we're excluding, skip it.
		if (reactElements[i] == exclude) continue;

		// If we are matching a specific key and this element's doesn't match it, skip it.
		if (key && strcmp(data->key, key)) continue;

		// Send the element the MSG_PROPERTY_CHANGED message.
		ElementMessage(reactElements[i], MSG_PROPERTY_CHANGED, 0, NULL);
	}
}

Let's add the button and label elements to the list when they're created.

Button *ReactU32ButtonCreate(Element *parent, int flags, const char *key, const char *label, 
		uint32_t minimum, uint32_t maximum, int32_t clickDelta) {
	// ... as before
	arrput(reactElements, &button->e); // NEW!
	return button;
}

Label *ReactU32DisplayCreate(Element *parent, int flags, const char *key, const char *prefix) {
	// ... as before
	arrput(reactElements, &label->e); // NEW!
	return label;
}

We'll now implement the MSG_PROPERTY_CHANGED in our message handlers. The button's is very simple, as we only need to request the element to be redrawn, and the new background color will be picked up automatically when the button is painted.

int ReactU32ButtonMessage(Element *element, Message message, int di, void *dp) {
	ReactData *data = (ReactData *) element->cp;

	if (message == MSG_DESTROY) {
		free(data);
	} else if (message == MSG_PROPERTY_CHANGED) {
		ElementRepaint(element, NULL); // NEW!
	} else if (message == MSG_BUTTON_GET_COLOR) {
		// ... as before
	}

	return 0;
}

To respond to this message in ReactU32DisplayMessage we need to both request the label to be repainted and actually update the text string it stores.

int ReactU32DisplayMessage(Element *element, Message message, int di, void *dp) {
	Label *label = (Label *) element;
	ReactData *data = (ReactData *) element->cp;

	if (message == MSG_DESTROY) {
		free(data);
	} else if (message == MSG_PROPERTY_CHANGED) {
		// Format the new content to a buffer.
		char buffer[64];
		snprintf(buffer, sizeof(buffer), "%s%d", 
				data->u32Display.prefix /* use the prefix string stored in the ReactData structure */,
				ObjectReadU32(SelectedObject(), data->key, 0) /* get the current value of the property */);

		// Set the content of the label element.
		LabelSetContent(label, buffer, -1);

		// Repaint the element.
		ElementRepaint(element, NULL);
	}

	return 0;
}

Unfortunately we've now got an unfortunate duplication and repetition of code. Indeed, the formatting of the display's string in now done in two locations, both in response to the MSG_PROPERTY_CHANGED message in the message handler, and in the ReactU32DisplayCreate function. But there's a simple trick we can use to avoid this. First, we remove the code to set the label's contents in the creation function, and leave the label initially blank.

Label *ReactU32DisplayCreate(Element *parent, int flags, const char *key, const char *prefix) {
	// Save the parameters.
	ReactData *data = (ReactData *) calloc(1, sizeof(ReactData));
	data->key = key;
	data->u32Display.prefix = prefix;

	// Create a blank label.
	Label *label = LabelCreate(parent, flags, NULL, 0); // UPDATED!

	// Save the data pointer and set the message handler.
	label->e.cp = data;
	label->e.messageUser = ReactU32DisplayMessage;

	// Save the label in the array and return it.
	arrput(reactElements, &label->e);
	return label;
}

And then here's the trick. Add the end of Populate, we call ReactUpdate to update all the elements in the list.

void Populate() {
	// ... as before

	ReactUpdate(NULL, NULL); // NEW!
	ElementMessage(container, MSG_LAYOUT, 0, 0);
	ElementRepaint(container, NULL);
}

And thus, all the labels that were created during the Populate function will be sent a MSG_PROPERTY_CHANGED message and they will set their contents to the correct string. Neat, huh?

We all this setup, we're now finally ready to start putting features back into our application. In the next article, we're going to add document modification and reintroduce interactivity into our application. All our hard work is about to pay off.

part21.c

Part 20: Saving data for reactive elements.

Part 22: Applying steps.