Notifications
Primer
Getting data-change notifications from hist is as simple as adding a method to your tracked class:
struct Item {
std::string label = "";
float value = 0.0f;
};
struct Npc_data {
std::string name = "";
int hitpoints = 0;
std::vector<Item> inventory {};
};
REFLECT_PRIVATE(Item, label, value)
REFLECT_PRIVATE(Npc_data, name, hitpoints, inventory)
struct Npc : nf::tracked<Npc_data, Npc>
{
Npc() : tracked(this) {}
using inventory_path = NF_PATH(root->inventory);
void element_added(inventory_path, std::size_t index)
{
std::cout << "new item: " << Json::out(read.inventory[index]) << '\n';
}
};
Note: since Npc is given as a template param to tracked, hist can perform compile-time checks to see whether certain methods like element_added exist, and if and only if they’re present, call them. Additionally because this is provided to tracked in the constructor, the call is not polymorphic.
npc()->inventory.append(Item{.name = "Bow", .value = 40.0f});
Undoing a removal or redoing an append would result in the same notification method being called.
Setting up a notification just requires that have an understanding of paths and notification method signatures.
Paths, Keys & Routes
-
A Path describes the traversal from the root of a class to one of its members/sub-members.
-
A Key is a map-key or random-access index (e.g. array or vector index)
-
A Route describes the traversal from the root of an object (/instance of a class) to one of its members/sub-members - routes are just instances of paths wherein keys are known.
Paths & routes are used in creating notifications and defining sub-elements; behind the scenes… routes to elements are serialized when calling an op (e.g. operator= and append), and deserialized when rendering history or calling undo/redo.
Paths are just types, it’s usually cleanest to create an alias for the paths you will be using with NF_PATH.
struct Npc : nf::tracked<Npc_data, Npc>
{
Npc() : tracked(this) {}
using name_path = NF_PATH(root->name);
using hitpoints_path = NF_PATH(root->hitpoints);
using inventory_path = NF_PATH(root->inventory);
using item_value_path = NF_PATH(root->inventory[0].value);
};
In this example, root is a static member of nf::tracked that you can use to create paths in an auto-complete friendly way. [0] here in inventory is merely syntactically representative of array access (and could in fact be any number), the path types do not store any key/index values.
An instance of a path (aka "route") does have the keys/index values, so if you say, had a change notification on an items value and need to know which items value changed, you can get that:
void value_changed(item_value_path path, float old_value, float new_value)
{
auto inventory_index = path.index<0>();
std::cout << "inventory[" << inventory_index << "].value changed from "
<< old_value << " to " << new_value << '\n';
}
Notification Signatures
Hist supports the "C.A.R.M.S." notifications - change, add, remove, move, and selection-update. Change is applicable to non-container fields, while add, remove, move, and selection update are only applicable to containers.
void value_changed(PATH, OLD_VALUE, NEW_VALUE);
void element_added(PATH, INDEX);
void element_removed(PATH, INDEX);
void element_moved(PATH, OLD_INDEX, NEW_INDEX);
void selections_changed(PATH);
void after_action(std::size_t action_index);
Unless you modified the index types (as described in Manual Optimizations), they’re going to be of type std::size_t, the values are the type of the actual value changed, and the paths are those you create with NF_PATH.
void value_changed(NF_PATH(root->hitpoints), int old_hitpoints, int new_hitpoints)
{
std::cout << "Hitpoints changed from " << old_hitpoints << " to " << new_hitpoints << '\n';
}
using inventory_path = NF_PATH(root->inventory);
using item_value_path = NF_PATH(root->inventory[0].value);
void value_changed(item_value_path item, float old_value, float new_value)
{
auto item_index = item.index<0>();
std::cout << "Item value at inventory index[" << item_index << "] changed from " << old_value << " to " << new_value << '\n';
}
void element_added(inventory_path, std::size_t index)
{
const auto & item = read.inventory[index];
std::cout << "Item added to inventory at index[" << index << "]: " << Json::out(item) << '\n';
}
void element_removed(inventory_path, std::size_t index)
{
const auto & item = read.inventory[index];
std::cout << "Removing item from inventory at index[" << index << "]: " << Json::out(item) << '\n';
}
void element_moved(inventory_path, std::size_t old_index, std::size_t new_index)
{
std::cout << "Inventory item moved from index " << old_index << " to " << new_index << '\n';
}
void selections_changed(inventory_path)
{
std::cout << "Inventory selections changed: " << Json::out(view.inventory.sel()) << '\n';
}
void after_action(std::size_t action_index)
{
std::cout << "Action submitted: [" << action_index << "]\n";
}