Let's add saving and loading to our test application. We start by adding two new buttons.

Panel *row = PanelCreate(&panel->e, ELEMENT_H_FILL | PANEL_HORIZONTAL);
LabelCreate(&row->e, 0, "Controls: ", -1);
ButtonCreate(&row->e, 0, "Undo", -1)->e.messageUser = ButtonUndoMessage;
ButtonCreate(&row->e, 0, "Redo", -1)->e.messageUser = ButtonRedoMessage;
ButtonCreate(&row->e, 0, "Save", -1)->e.messageUser = ButtonSaveMessage; // NEW!
ButtonCreate(&row->e, 0, "Load", -1)->e.messageUser = ButtonLoadMessage; // NEW!

And add the boilerplate for their click handlers.

BUTTON_HANDLE_CLICK_PROLOGUE(ButtonSaveMessage) {
 	FILE *f = fopen("tutorial.dat", "wb");

	// TODO Save the document to the file.

 	fclose(f);
} BUTTON_HANDLE_CLICK_EPILOGUE()

BUTTON_HANDLE_CLICK_PROLOGUE(ButtonLoadMessage) {
 	FILE *f = fopen("tutorial.dat", "rb");
 	if (!f) return 0;

	// Free the old document before loading the new one.
	DocumentFree();

	// TODO Load the document from the file.

 	fclose(f);

	// Recreate the user interface after loading the file.
 	Populate();
} BUTTON_HANDLE_CLICK_EPILOGUE()

We're not going to worry about showing file dialogs and management of the document path, and neither will we worry about handling IO and memory errors. That's all out of scope here.

Because everything is in generic property lists, saving and loading becomes surprisingly easy. Saving especially.

// Save the object ID allocator.
fwrite(&objectIDAllocator, 1, sizeof(uint64_t), f);

// Save the number of objects in the document.
uint32_t objectCount = hmlenu(objects);
fwrite(&objectCount, 1, sizeof(uint32_t), f);

// For each object...
for (int i = 0; i < hmlen(objects); i++) {
	// Save the object's key, type and number of properties.
	fwrite(&objects[i].key, 1, sizeof(uint64_t), f);
	uint32_t type = objects[i].value.type;
	fwrite(&type, 1, sizeof(uint32_t), f);
	uint32_t propertyCount = arrlen(objects[i].value.properties);
	fwrite(&propertyCount, 1, sizeof(uint32_t), f);

	// For each property in the object...
	for (uint32_t j = 0; j < propertyCount; j++) {
		// Save the property's type annd key.
		fwrite(&objects[i].value.properties[j].type, 1, sizeof(uint8_t), f);
		fwrite(&objects[i].value.properties[j].key, 1, PROPERTY_KEY_MAX_SIZE + 1, f);

		// Save the property's value. Dependent on the type.
		if (objects[i].value.properties[j].type == PROPERTY_U32) {
			fwrite(&objects[i].value.properties[j].u32, 1, sizeof(uint32_t), f);
		} else {
			// ...
		}
	}
}

And that's all there is to saving. Loading involves a little bit more work, but not much more.

// Load the object ID allocator.
fread(&objectIDAllocator, 1, sizeof(uint64_t), f);

// Load the number of objects in the file.
uint32_t objectCount = 0;
fread(&objectCount, 1, sizeof(uint32_t), f);

// For each object...
for (uint32_t i = 0; i < objectCount; i++) {
	Object object = {};

	// Read the ID, type and number of properties.
	uint64_t id = 0;
	fread(&id, 1, sizeof(uint64_t), f);
	uint32_t type = 0;
	fread(&type, 1, sizeof(uint32_t), f);
	uint32_t propertyCount = 0;
	fread(&propertyCount, 1, sizeof(propertyCount), f);
	object.type = type;

	// Make sure that object ID allocator is actually set to a valid value.
	if (objectIDAllocator < id) {
		objectIDAllocator = id;
	}

	// For each property...
	for (uint32_t j = 0; j < propertyCount; j++) {
		// Read the type and key of the property.
		Property property = {};
		fread(&property.type, 1, sizeof(property.type), f);
		fread(&property.key, 1, PROPERTY_KEY_MAX_SIZE + 1, f);
		property.key[PROPERTY_KEY_MAX_SIZE] = 0;

		// Read the property's value. Dependent on the type.
		if (property.type == PROPERTY_U32) {
			fread(&property.u32, 1, sizeof(uint32_t), f);
		} else {
			// ...
		}

		// Add the property to the array.
		arrput(object.properties, property);
	}

	// Add the object to the map.
	hmput(objects, id, object);

	// If this is a counter object, set it to be the selected object.
	if (type == OBJECT_COUNTER) {
		selectedObjectID = id;
	}
}

And our sample application is finished! :) If you're extending the application, the only time you'll need to come back to these functions is to add support for more property types.

The same screenshot as in the previous article, except there is now a Save and a Load button.

So, now that we've seen this architecture for storing document state and updating it, I'd like to say a few closing words about when you should use it. Obviously, it was overkill for this simple test application, but I've found it to be very useful in my exploits. For example, I made a little developer tool for designing UI themes using it.

Screenshot of Designer showing an inspector with various properties and the layer graph.

The great thing about this system though is that you can, and should, adapt it for your application's specific needs. I don't think you should try to make a library out of it, as it is too tightly coupled with your application. It's highly useful to be able to introduce property types for all the things you need without worrying about complicating a generic library.

That's all for now, folks.

part24.c

Part 23: Undo and redo.