It's time to implement undo and redo! Because of our Step
-based architecture, this is going to be pretty straightforward. In fact, the most awkward part of this article is going to be created the buttons to actually invoke the undo and redo actions.
As I mentioned earlier, we never put a nice way of responding to button presses in our UI library, so it's all a bit verbose. So to remedy this, let's write two macros to implement the boilerplate of a button's message handler.
#define BUTTON_HANDLE_CLICK_PROLOGUE(name) \
int name(Element *element, Message message, int di, void *dp) { \
if (message == MSG_CLICKED) {
#define BUTTON_HANDLE_CLICK_EPILOGUE() \
} \
return 0; \
}
We're going to be adding an undo and a redo button, so let's get the message handlers setup and ready to implement.
BUTTON_HANDLE_CLICK_PROLOGUE(ButtonUndoMessage) {
} BUTTON_HANDLE_CLICK_EPILOGUE()
BUTTON_HANDLE_CLICK_PROLOGUE(ButtonRedoMessage) {
} BUTTON_HANDLE_CLICK_EPILOGUE()
And now let's create the buttons and add them to their own row. In the main
function, we put:
int main() {
// ... as before
Initialise();
Window *window = WindowCreate("Hello, world", 400, 120);
Panel *panel = PanelCreate(&window->e, PANEL_GRAY);
panel->border = RectangleMake(10, 10, 10, 10);
panel->gap = 15;
// NEW!
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;
container = &PanelCreate(&panel->e, ELEMENT_H_FILL | ELEMENT_V_FILL)->e;
Populate();
int result = MessageLoop();
DocumentFree();
return result;
}
Here's what the interface looks like now:
We're ready to get stuck into undo! We're going to store two lists of Step
s, one containing the list of steps for undo, and one containing the list of steps for redo. As usual, these'll be powered by stb_ds.h
for simplicity.
Step *undoStack, *redoStack;
Let's not forget to clear and free these in DocumentFree
.
for (int i = 0; i < arrlen(undoStack); i++) {
StepFree(undoStack[i]);
}
for (int i = 0; i < arrlen(redoStack); i++) {
StepFree(redoStack[i]);
}
arrfree(undoStack);
arrfree(redoStack);
We next turn our attention towards StepApply
. When a step is performed, we're going to push the 'reverse' step to the undo stack. By 'reverse' step, I mean a step that will revert the state of the document to what it was before the step was applied. This of course depends on the type of step; let's implement it for STEP_SET_PROPERTY
.
// ... as before
Object *object = SelectedObject();
Step reverse = { 0 };
const char *updateKey = NULL;
if (step.type == STEP_SET_PROPERTY) {
// NEW!
// The reverse step of setting a property is really simple;
// we duplicate the step and modify the property in the step so it's the old value.
reverse = step;
reverse.property = ObjectReadAny(object, step.property.key);
// ... as before
}
// ... as before
With that, we can then push the reverse
step onto the undoStack
. However, we also need to clear the redoStack
when this happens. If you hadn't noticed before, whenever you perform an action in a document based application, the redo stack will be cleared. (Except I think in Emacs, which does its own thing.)
// Clear the redo stack.
for (int i = 0; i < arrlen(redoStack); i++) StepFree(redoStack[i]);
arrfree(redoStack);
// Push the reverse step onto the undo stack.
arrput(undoStack, reverse);
Now we need to actually implement what happens when the undo and redo buttons are pressed. When the user clicks the undo button, we need to remove the last step from the undo stack, apply it, and then put the reverse on the redo stack. When the user clicks the redo button, we need to remove the last step from the redo stack, apply it, and then put the reverse on the undo stack. For this, we introduce 3 mode constants for the StepApply
functions.
#define STEP_APPLY_NORMAL (0)
#define STEP_APPLY_UNDO (1)
#define STEP_APPLY_REDO (2)
void StepApply(Step step, Element *updateExclude,
int mode /* NEW */) {
// ...
}
int ReactU32ButtonMessage(Element *element, Message message, int di, void *dp) {
// ...
if (message == MSG_CLICKED) {
// ...
StepApply(step, NULL, STEP_APPLY_NORMAL /* NEW */);
// ...
}
// ...
}
In StepApply
we then change what we do with the reverse
step depending on the mode
.
if (mode == STEP_APPLY_NORMAL) {
// The logic from before.
// Clear the redo stack and put the reverse step on the undo stack.
for (int i = 0; i < arrlen(redoStack); i++) StepFree(redoStack[i]);
arrfree(redoStack);
arrput(undoStack, reverse);
} else if (mode == STEP_APPLY_UNDO) {
// We just undid an action, so put the reverse step on the redo stack.
arrput(redoStack, reverse);
} else if (mode == STEP_APPLY_REDO) {
// We just redid an action, so put the reverse step on the undo stack.
arrput(undoStack, reverse);
}
Finally, we finish off the undo and redo button click handlers.
BUTTON_HANDLE_CLICK_PROLOGUE(ButtonUndoMessage) {
// Are there any steps available to undo?
if (arrlen(undoStack)) {
// Remove the last step from the undo stack and apply it.
StepApply(arrpop(undoStack), NULL, STEP_APPLY_UNDO);
}
} BUTTON_HANDLE_CLICK_EPILOGUE()
BUTTON_HANDLE_CLICK_PROLOGUE(ButtonRedoMessage) {
// Are there any steps available to redo?
if (arrlen(redoStack)) {
// Remove the last step from the redo stack and apply it.
StepApply(arrpop(redoStack), NULL, STEP_APPLY_REDO);
}
} BUTTON_HANDLE_CLICK_EPILOGUE()
As you can see, undo and redo fits really nicely into our step and property systems. We've got a memory efficient way of reversing and redoing state changes, and the UI automatically synchronizes with it all.
Next article will be last in this tutorial series, where we have a go at serialization.