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.

Now whenever elements are added to inventory this method will get called, e.g. Run

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';
}
Run

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";
}
Run