Create a Custom Asset Type: Part 2

This part will cover the following topics:

  • How to store data in a buffer that is associated with the asset file.
  • How to give the asset a custom UI in the Property View.

The next part shows how to write an importer for the asset.

You can find the whole source code in its git repo: example-text-file-asset

Table of Content

Adding More Properties to the The Truth Type

The Truth type we created in Part 1 cannot do much, because it doesn't have any properties:

static void create_truth_types(struct tm_the_truth_o *tt) {
  // we have properties this is why the last arguments are "0, 0"
  const tm_tt_type_t type =
      tm_the_truth_api->create_object_type(tt, TM_TT_TYPE__MY_ASSET, 0, 0);
  tm_tt_set_aspect(tt, type, tm_tt_assets_file_extension_aspect_i, "my_asset");
}

To actually store some data in the objects, we want to add some properties to the Truth type. Note that we pass in an array of properties when we create the type with tm_the_truth_api->create_object_type().

For our text file objects that are two pieces of data that we want to store:

  1. The text data itself.
  2. The path on disk (if any) that the text file was imported from.

Storing the import path is not strictly necessary, but we'll use it to implement a "reimport" feature. This lets our data type work nicely with text files that are edited in external programs.

Here's how we can define these properties:

static tm_the_truth_property_definition_t my_asset_properties[] = {
    {"import_path", TM_THE_TRUTH_PROPERTY_TYPE_STRING},
    {"data", TM_THE_TRUTH_PROPERTY_TYPE_BUFFER},
};

Note: The type tm_the_truth_property_definition_t has a lot more options. For example, is it possible to hide properties from the editor, etc. For more information, read the documentation here.

In this case we decided to store the text as a buffer instead of a string. Buffers can be streamed in and out of memory easily, so if we expect the text files to be large, using a buffer makes more sense than using a string.

We can now create the Truth type with these properties:

static void create_truth_types(struct tm_the_truth_o *tt) {
  tm_tt_set_aspect(tt, type, tm_tt_assets_file_extension_aspect_i, "txt");
}

Let's also change the asset name to something more meaningful than my_asset. We'll call it txt. We need to update this new name in four places:

  • Asset Name
  • Menu Name
  • File extension
  • The source file: my_asset.c/h -> txt.c/h

This will change the code as follows:

static void create_truth_types(struct tm_the_truth_o *tt) {
  tm_tt_set_aspect(tt, type, tm_tt_assets_file_extension_aspect_i, "txt");
}
// .. other code
static tm_asset_browser_create_asset_i asset_browser_create_my_asset = {
    .menu_name = TM_LOCALIZE_LATER("New Text File"),
    .asset_name = TM_LOCALIZE_LATER("New Text File"),
    .create = asset_browser_create,
};
}

Let's have a look at how it looks in the editor:

creating a new asset

If we create a new Text file and select it, this is what we will see in the Properties View:

The Data property is nil because we haven't loaded any data into the file yet. Let's add a UI that let's us import text files from disk.

(Another option would be to add a Text Editor UI that would let us edit the text data directly in the editor. However, writing a good text editor is a big task, so for this tutorial, let's use an import workflow instead.)

Custom UI

To show an Import button in the Properties View, we need to customize the Properties View UI of our type. We can do this by adding a TM_TT_ASPECT__PROPERTIES to the Truth type.

The TM_TT_ASPECT__PROPERTIES is implemented with a tm_properties_aspect_i struct. This struct has a lot of field that can be used to customize various parts of the Properties View (for more information on them, check out the documentation). For our purposes, we are interested in the custom_ui() field that lets us use a custom callback for drawing the type in the Properties View.

custom_ui() wants a function pointer of the type float (*custom_ui)(struct tm_properties_ui_args_t *args, tm_rect_t item_rect, tm_tt_id_t object).

Let us quickly go over this:

ArgumentData TypeDescription
argstm_properties_ui_args_tA struct with information from the Properties View that can be used in drawing the UI. For example, this has the ui instance as well as the uistyle which you will need in any tm_ui_api calls. For more information check the documentation.
item_recttm_rect_tThe rect in the Properties View UI where the item should be drawn. Note that the height in this struct (item_rect.h) is the height of a standard property field. You can use more or less height to draw your type as long as you return the right y value (see below).
objecttm_tt_id_tThe ID of the Truth object that the Properties View wants to draw.
Return valueDescription
floatReturns the y coordinate where the next item in the Propertiew View should be drawn. This should be item_rect.y + however much vertical space your controls are using.

To implement the custom_ui() function we can make use of the functions for drawing property UIs found in tm_properties_view_api, or we can draw UI directly using tm_ui_api. Once we've implemented custom_ui() we need a instance of tm_properties_aspect_i to register. This instance must have global lifetime so it doesn't get destroyed:

//.. other code
static float properties__custom_ui(struct tm_properties_ui_args_t *args,
                                   tm_rect_t item_rect, tm_tt_id_t object) {
  return item_rect.y;
}
static tm_properties_aspect_i properties_aspect = {
    .custom_ui = properties__custom_ui,
};
// .. other code

Now we can register this aspect with tm_truth_api:

//.. other code

static void create_truth_types(struct tm_the_truth_o *tt) {
  static tm_properties_aspect_i properties_aspect = {
      .custom_ui = properties__custom_ui,
  };
  tm_tt_set_aspect(tt, type, tm_properties_aspect_i, &properties_aspect);
}
//... the other code

In the editor, the change is imminently visible. The UI is gone, because it is now using our custom_ui() function, but our custom_ui() function isn't drawing anything.

Let's add the Imported Path property back to the UI. We can look at the properties view API for a suitable function to draw this property (if we can't find anything we may have to write a custom drawer ourselves).

We could use tm_properties_view_api->ui_property_default(). This would use the default editor based on the property type. For a STRING property, this is just a text edit field, the same thing that we saw before implementing our custom_ui() function. (If we don't have a custom UI, the default UI for each property will be used.)

We chould also use tm_properties_view_api->ui_string(). This is just another way of drawing the default STRING UI.

But for our purposes, tm_properties_view_api->ui_open_path() is better. This is a property UI specifically for file system path. It will draw a button, and if you click the button a system file dialog is shown that let's you pick a path.

Note that in order to use tm_properties_view_api we need to load it in our tm_load_plugin() function:

static struct tm_properties_view_api *tm_properties_view_api;
#include <plugins/editor_views/properties.h>
TM_DLL_EXPORT void tm_load_plugin(struct tm_api_registry_api *reg, bool load) {
  tm_properties_view_api = tm_get_api(reg, tm_properties_view_api);
}

Now we can call ui_open_path() . Let's start by looking at its signature:

float (*ui_open_path)(struct tm_properties_ui_args_t *args, tm_rect_t item_rect, const char *name, const char *tooltip, tm_tt_id_t object, uint32_t property, const char *extensions, const char *description, bool *picked)

ArgumentData TypeDescription
argstm_properties_ui_args_tFor this argument, we should pass along the args pointer we got in our custom_ui() function.
item_recttm_rect_tThe rect where we want the UI of the control to be drawn (including the label).
nameconst char*The label that the Properties View UI will display in front of the button.
tooltipconst char*Tooltip that will be shown if the mouse is hovered over the label.
objecttm_tt_id_tThe Truth object that holds the path STRING that should be edited.
propertyuint32_tThe index of the STRING property that should be edited.
extensionconst char*List of file extensions that the open file dialog should show (separated by space).
descriptionconst char*Description of the file to open shown in the open file dialog.
pickedbool*Optional out pointer that is set to true if a new file was picked in the file dialog.
Return valueDescription
floatThe y coordinate where the next property should be drawn.

We can now implement the function:

bool picked = false;
item_rect.y = tm_properties_view_api->ui_open_path(
    args, item_rect, TM_LOCALIZE_LATER("Import Path"),
    TM_LOCALIZE_LATER("Path that the text file was imported from."), object,
    TM_TT_PROP__MY_ASSET__FILE, "txt", "text files", &picked);
if (picked) {
}

Note that we are using the property index TM_TT_PROP__MY_ASSET__FILE that we defined in the header file hearlier:

#pragma once
#include <foundation/api_types.h>
//... more code
#define TM_TT_TYPE__MY_ASSET "tm_my_asset"
#define TM_TT_TYPE_HASH__MY_ASSET TM_STATIC_HASH("tm_my_asset", 0x1e12ba1f91b99960ULL)

enum
{
    TM_TT_PROP__MY_ASSET__FILE,
    TM_TT_PROP__MY_ASSET__DATA,
};

We can now test this in the engine. We see an Import Path label with a button and when we click it, we get asked to import a file.

Next, we want to make sure that when the user picks a file using this method, we load the file and store it in our DATA buffer.

To load files we can use the tm_os_api which gives us access to OS functionality. tm_os_api has a lot of sub-APIs for different purposes (files, memory, threading, etc). In our case, what we need is tm_os_api->file_io which provides access to File I/O functionality:

//other includes
#include <foundation/os.h>
#include <foundation/buffer.h>
//.. other code
static float properties__custom_ui(struct tm_properties_ui_args_t *args,
                                   tm_rect_t item_rect, tm_tt_id_t object) {
  return item_rect.y;
}
static tm_properties_aspect_i properties_aspect = {
    .custom_ui = properties__custom_ui,
};

When a new file is picked in the UI (checked with the picked variable) we get the file path from The Truth, read the file data and store it in The Truth.

To manage buffers, we make use of the interface in buffers.h. Creating a buffer is a three step process:

  • Allocating the memory for the buffer (based on the file size).
  • Filling the buffer with content (in this case, from the text file).
  • Adding the buffer to the tm_buffers_i object.

Once we have created the buffer, we need to set the BUFFER data item in the Truth object to this buffer. Changing a value in The Truth is another three step process:

  • Ask the Truth for a write pointer to the object using write().
  • Set the buffer for the write pointer using set_buffer().
  • Commit the changes to the Truth using commit().

We need this somewhat complicated procedure because objects in The Truth are immutable by default. This ensures that The Truth can be used from multiple threads simulatenously. When you change a Truth object using the write()/commit() protocol, the changes are applied atomically. I.e., other threads will either see the old Truth object or the new one, never a half-old, half-new object.

If you want the change to go into the undo stack so that you can revert it with Edit → Undo, you need some additional steps:

  • Create an undo scope for the action using create_undo_scope().
  • Pass that undo scope into commit().
  • Register the undo scope with the application's undo stack (found in args->undo_stack).

To simplify this example, we've skipped that step and instead we use TM_TT_NO_UNDO_SCOPE for the commit() action which means the action will not be undoable.

What Is Next?

In the next part we'll show how to add an Importer for our asset type. This will let us drag and drop text files from the explorer into the asset browser.

Part 3

Full Example of Basic Asset

my_asset.h

#pragma once
#include <foundation/api_types.h>
//... more code
#define TM_TT_TYPE__MY_ASSET "tm_my_asset"
#define TM_TT_TYPE_HASH__MY_ASSET TM_STATIC_HASH("tm_my_asset", 0x1e12ba1f91b99960ULL)

enum
{
    TM_TT_PROP__MY_ASSET__FILE,
    TM_TT_PROP__MY_ASSET__DATA,
};

(Do not forget to run hash.exe when you create a TM_STATIC_HASH)

my_asset.c

// -- api's
static struct tm_the_truth_api *tm_the_truth_api;
static struct tm_properties_view_api *tm_properties_view_api;
static struct tm_os_api *tm_os_api;
// -- inlcudes
#include <foundation/api_registry.h>
#include <foundation/buffer.h>
#include <foundation/localizer.h>
#include <foundation/macros.h>
#include <foundation/os.h>
#include <foundation/the_truth.h>
#include <foundation/the_truth_assets.h>
#include <foundation/undo.h>

#include <plugins/editor_views/asset_browser.h>
#include <plugins/editor_views/properties.h>

#include "txt.h"

//custom ui
static float properties__custom_ui(struct tm_properties_ui_args_t *args, tm_rect_t item_rect, tm_tt_id_t object)
{
    tm_the_truth_o *tt = args->tt;
    bool picked = false;
    item_rect.y = tm_properties_view_api->ui_open_path(args, item_rect, TM_LOCALIZE_LATER("Import Path"), TM_LOCALIZE_LATER("Path that the text file was imported from."), object, TM_TT_PROP__MY_ASSET__FILE, "txt", "text files", &picked);
    if (picked)
    {
        const char *file = tm_the_truth_api->get_string(tt, tm_tt_read(tt, object), TM_TT_PROP__MY_ASSET__FILE);
        tm_file_stat_t stat = tm_os_api->file_system->stat(file);
        tm_buffers_i *buffers = tm_the_truth_api->buffers(tt);
        void *buffer = buffers->allocate(buffers->inst, stat.size, false);
        tm_file_o f = tm_os_api->file_io->open_input(file);
        tm_os_api->file_io->read(f, buffer, stat.size);
        tm_os_api->file_io->close(f);
        const uint32_t buffer_id = buffers->add(buffers->inst, buffer, stat.size, 0);
        tm_the_truth_object_o *asset_obj = tm_the_truth_api->write(tt, object);
        tm_the_truth_api->set_buffer(tt, asset_obj, TM_TT_PROP__MY_ASSET__DATA, buffer_id);
        tm_the_truth_api->commit(tt, asset_obj, TM_TT_NO_UNDO_SCOPE);
    }
    return item_rect.y;
}
// -- create truth type
static void create_truth_types(struct tm_the_truth_o *tt)
{
    static tm_the_truth_property_definition_t my_asset_properties[] = {
        {"import_path", TM_THE_TRUTH_PROPERTY_TYPE_STRING},
        {"data", TM_THE_TRUTH_PROPERTY_TYPE_BUFFER},
    };
    const tm_tt_type_t type = tm_the_truth_api->create_object_type(tt, TM_TT_TYPE__MY_ASSET, my_asset_properties, TM_ARRAY_COUNT(my_asset_properties));
    tm_tt_set_aspect(tt, type, tm_tt_assets_file_extension_aspect_i, "txt");
    static tm_properties_aspect_i properties_aspect = {
        .custom_ui = properties__custom_ui,
    };
    tm_tt_set_aspect(tt, type, tm_properties_aspect_i, &properties_aspect);
}

// -- asset browser regsiter interface
static tm_tt_id_t asset_browser_create(struct tm_asset_browser_create_asset_o *inst, tm_the_truth_o *tt, tm_tt_undo_scope_t undo_scope)
{
    const tm_tt_type_t type = tm_the_truth_api->object_type_from_name_hash(tt, TM_TT_TYPE_HASH__MY_ASSET);
    return tm_the_truth_api->create_object_of_type(tt, type, undo_scope);
}
static tm_asset_browser_create_asset_i asset_browser_create_my_asset = {
    .menu_name = TM_LOCALIZE_LATER("New Text File"),
    .asset_name = TM_LOCALIZE_LATER("New Text File"),
    .create = asset_browser_create,
};
// -- load plugin
TM_DLL_EXPORT void tm_load_plugin(struct tm_api_registry_api *reg, bool load)
{
    tm_the_truth_api = tm_get_api(reg, tm_the_truth_api);
    tm_properties_view_api = tm_get_api(reg, tm_properties_view_api);
    tm_os_api = tm_get_api(reg, tm_os_api);
    tm_add_or_remove_implementation(reg, load, tm_the_truth_create_types_i, create_truth_types);
    tm_add_or_remove_implementation(reg, load, tm_asset_browser_create_asset_i, &asset_browser_create_my_asset);
}