cOMS/ui/UILayout.h

593 lines
18 KiB
C

#ifndef TOS_UI_LAYOUT_H
#define TOS_UI_LAYOUT_H
#include <string.h>
#include "../stdlib/Types.h"
#include "../stdlib/HashMap.h"
#include "../asset/Asset.h"
#include "../camera/Camera.h"
#include "../system/FileUtils.cpp"
#include "UITheme.h"
#include "UIElement.h"
#include "UIElementType.h"
#define UI_LAYOUT_VERSION 1
// Modified for every scene
struct UILayout {
// This array has the size of the game window and represents in color codes where interactible ui elements are
// Size is based on screen size (we don't need full screen size since we assume an interactible element is at least 4 pixels width and height)
// width = 25% of screen size
// height = 25% of screen size
uint16 width;
uint16 height;
// Contains all UI elements also dynamic ones (e.g. movable windows)
uint32* ui_chroma_codes;
// Used to directly find element by name
// The values are pointers to the UIElements
// @todo Should be a perfect hash map
HashMap hash_map;
// UI data
// Only the size of the fixed layout, doesn't include the theme specific data
uint32 layout_size;
// Total possible size
uint32 data_size;
// Holds the ui elements
// The structure of the data is as follows:
// 1. HashMap data (points to 2.a.)
// 2. UIElements (default)
// a. General UIElement
// b. Element specific state
// c. Element specific style
// 3. Additional UI styles (see c.), dynamically created when the theme is loaded
// We effectively create a tree in data where the individual elements can get directly accessed through the hashmap
byte* data;
// Changes on a as needed basis
uint32 vertex_size_static;
bool static_content_changed;
// Changes every frame
uint32 vertex_size_dynamic;
bool dynamic_content_changed;
// Contains both static and dynamic content
// @todo The vertices shouldn't be an Asset, it's more like a ECS, maybe it's not even in RAM and only in VRAM?!
// One of the reasons for this being an asset is also that it is easy to log ram/vram usage but that can be fixed
Asset* ui_asset;
// Total count of the ui_asset vertices
uint32 vertex_size;
// @question Should we maybe also hold the font atlas asset here AND the color palette?
};
void count_direct_child_elements(UIElement* element, const char* pos, int32 parent_level)
{
// Find amount of child elements
// We have to perform a lookahead since this determins the since this determines the size of our current element
uint16 direct_child_elements = 0;
int32 level = 0;
while (*pos != '\0') {
while (*pos == ' ' || *pos == '\t') {
++pos;
++level;
}
if (level > parent_level + 4) {
// This element is a childrens child and not a direct child
continue;
} else if (level <= parent_level) {
// We are no longer inside of element
break;
}
}
element->children_count = direct_child_elements;
}
void assign_child_elements(UILayout* layout, UIElement* element, char* pos, int32 parent_level) {
int32 current_child_pos = 0;
char block_name[28];
int32 level = 0;
while (*pos != '\0') {
while (*pos == ' ' || *pos == '\t') {
++pos;
++level;
}
if (level > parent_level + 4) {
// This element is a childrens child and not a direct child
continue;
} else if (level <= parent_level) {
// We are no longer inside of element
break;
}
str_copy_move_until(&pos, block_name, ":");
str_move_past(&pos, '\n');
element->children[current_child_pos] = (UIElement *) (
layout->data
+ ((HashEntryInt64 *) hashmap_get_entry(&layout->hash_map, block_name))->value
);
element->children[current_child_pos]->parent = element;
++current_child_pos;
}
}
// WARNING: theme needs to have memory already reserved and assigned to data
void layout_from_file_txt(
UILayout* layout,
const char* path,
RingMemory* ring
) {
FileBody file;
file_read(path, &file, ring);
ASSERT_SIMPLE(file.size);
char* pos = (char *) file.content;
// move past the version string
pos += 8;
// Use version for different handling
int32 version = strtol(pos, &pos, 10); ++pos;
// 1. Iteration: We have to find how many elements are defined in the layout file.
// Therefore we have to do an initial iteration
int32 temp_element_count = 0;
while (*pos != '\0') {
// Skip all white spaces
str_skip_empty(&pos);
++temp_element_count;
// Go to the next line
str_move_past(&pos, '\n');
}
// 2. Iteration: Fill HashMap
// @performance This is probably horrible since we are not using a perfect hashing function (1 hash -> 1 index)
// I wouldn't be surprised if we have a 50% hash overlap (2 hashes -> 1 index)
hashmap_create(&layout->hash_map, temp_element_count, sizeof(HashEntryVoidP), layout->data);
int64 hm_size = hashmap_size(&layout->hash_map);
pos = (char *) file.content;
// move past version string
str_move_past(&pos, '\n');
char block_name[28];
char block_type[28];
byte* element_data = layout->data + hm_size;
int32 level;
while (*pos != '\0') {
if (*pos == '\n') {
++pos;
continue;
}
level = 0;
while (*pos == ' ' || *pos == '\t') {
++pos;
++level;
}
if (*pos == '\n' || *pos == '\0') {
continue;
}
str_copy_move_until(&pos, block_name, ":"); ++pos;
str_copy_move_until(&pos, block_type, " \r\n");
str_move_past(&pos, '\n');
UIElement* element = (UIElement *) element_data;
element->type = (UIElementType) ui_element_type_to_id(block_type);
element->children = (UIElement **) (element_data + sizeof(UIElement));
count_direct_child_elements(element, pos, level);
// Every UIElement can have child elements, we need to store the pointers to those elements
element_data += sizeof(UIElement) + sizeof(UIElement*) * element->children_count;
// We also put the state data after this element
element->state = element_data;
element_data += ui_element_state_size(element->type);
// We also put the default element data after this element
// Depending on the theme we will have also additional styles (e.g. :active, :hidden, ...)
element->details[0] = element_data;
// @performance We should probably make sure the data is nicely aligned here
element_data += ui_element_type_size(element->type);
// Insert new element
hashmap_insert(&layout->hash_map, block_name, element_data - layout->data);
}
// 3. Iteration: Create child references
pos = (char *) file.content;
// move past version string
str_move_past(&pos, '\n');
while (*pos != '\0') {
if (*pos == '\n') {
++pos;
continue;
}
level = 0;
while (*pos == ' ' || *pos == '\t') {
++pos;
++level;
}
if (*pos == '\n' || *pos == '\0') {
continue;
}
str_copy_move_until(&pos, block_name, ":");
str_move_past(&pos, '\n');
UIElement* element = (UIElement *) (layout->data + ((HashEntryInt64 *) hashmap_get_entry(&layout->hash_map, block_name))->value);
assign_child_elements(layout, element, pos, level);
}
}
static inline
void ui_layout_serialize_element(HashEntryInt64* entry, byte* data, byte** pos, const byte* start)
{
// @performance Are we sure the data is nicely aligned?
// Probably depends on the from_txt function and the start of layout->data
UIElement* element = (UIElement *) (data + entry->value);
**pos = element->state_flag;
*pos += sizeof(element->state_flag);
**pos = element->type;
*pos += sizeof(element->type);
**pos = element->style_old;
*pos += sizeof(element->style_old);
**pos = element->style_new;
*pos += sizeof(element->style_new);
// Parent
if (element->parent) {
*((uint64 *) *pos) = SWAP_ENDIAN_LITTLE((uint64) ((uintptr_t) element->parent - (uintptr_t) data));
} else {
memset(*pos, 0, sizeof(uint64));
}
*pos += sizeof(uint64);
// State
if (element->state) {
*((uint64 *) *pos) = SWAP_ENDIAN_LITTLE((uint64) ((uintptr_t) element->state - (uintptr_t) data));
} else {
memset(*pos, 0, sizeof(uint64));
}
*pos += sizeof(uint64);
// Details
for (int32 j = 0; j < UI_STYLE_TYPE_SIZE; ++j) {
if (element->details[j]) {
*((uint64 *) *pos) = SWAP_ENDIAN_LITTLE((uint64) ((uintptr_t) element->details[j] - (uintptr_t) data));
} else {
memset(*pos, 0, sizeof(uint64));
}
*pos += sizeof(uint64);
}
*((uint16 *) *pos) = SWAP_ENDIAN_LITTLE(element->children_count);
*pos += sizeof(element->children_count);
for (int32 j = 0; j < element->children_count; ++j) {
*((uint64 *) *pos) = SWAP_ENDIAN_LITTLE((uint64) ((uintptr_t) element->children[j] - (uintptr_t) data));
*pos += sizeof(uint64);
}
// @performance, shouldn't it just be memset for both elements?
// state element data e.g. UIInputState
memcpy(*pos, element->state, ui_element_state_size(element->type));
*pos += ui_element_state_size(element->type);
// detailed element data e.g. UIInput
memcpy(*pos, element->details[0], ui_element_type_size(element->type));
*pos += ui_element_type_size(element->type);
}
int32 layout_to_data(
const UILayout* layout,
byte* data
) {
byte* pos = data;
byte* max_pos = data;
// version
*((int32 *) pos) = SWAP_ENDIAN_LITTLE(UI_LAYOUT_VERSION);
pos += sizeof(int32);
// hashmap
byte* start = pos;
pos += hashmap_dump(&layout->hash_map, pos);
// UIElement data
for (uint32 i = 0; i < layout->hash_map.buf.count; ++i) {
if (!layout->hash_map.table[i]) {
continue;
}
HashEntryInt64* entry = (HashEntryInt64 *) layout->hash_map.table[i];
pos = start + entry->value;
ui_layout_serialize_element(entry, layout->data, &pos, start);
if (pos > max_pos) {
max_pos = pos;
}
// save all the next elements
while (entry->next) {
pos = start + entry->value;
ui_layout_serialize_element(entry, layout->data, &pos, start);
if (pos > max_pos) {
max_pos = pos;
}
entry = entry->next;
}
}
return (int32) (max_pos - data);
}
static inline
void ui_layout_parse_element(HashEntryInt64* entry, byte* data, const byte** pos)
{
// @performance Are we sure the data is nicely aligned?
// Probably depends on the from_txt function and the start of layout->data
// Change offset to pointer
entry->value = (uintptr_t) data + entry->value;
UIElement* element = (UIElement *) entry->value;
element->state_flag = **pos;
*pos += sizeof(element->state_flag);
element->type = (UIElementType) **pos;
*pos += sizeof(element->type);
element->style_old = (UIStyleType) **pos;
*pos += sizeof(element->style_old);
element->style_new = (UIStyleType) **pos;
*pos += sizeof(element->style_new);
// Parent
uint64 temp = SWAP_ENDIAN_LITTLE(*((uint64 *) *pos));
element->parent = temp
? (UIElement *) (data + temp)
: NULL;
*pos += sizeof(uint64);
// State
temp = SWAP_ENDIAN_LITTLE(*((uint64 *) *pos));
element->state = temp
? data + temp
: NULL;
*pos += sizeof(uint64);
// Details
for (int32 j = 0; j < UI_STYLE_TYPE_SIZE; ++j) {
temp = SWAP_ENDIAN_LITTLE(*((uint64 *) *pos));
element->details[j] = temp
? data + temp
: NULL;
*pos += sizeof(uint64);
}
element->children_count = SWAP_ENDIAN_LITTLE(*((uint16 *) *pos));
*pos += sizeof(element->children_count);
for (int32 j = 0; j < element->children_count; ++j) {
temp = SWAP_ENDIAN_LITTLE(*((uint64 *) *pos));
element->children[j] = temp
? (UIElement *) (data + temp)
: NULL; // this should never happen since the children_count only gets incremented if it has a child
if (element->children[j]) {
element->children[j]->parent = element;
}
*pos += sizeof(uint64);
}
// @performance, shouldn't it just be memset for both elements?
// state element data e.g. UIInput
memcpy(element->state, *pos, ui_element_state_size(element->type));
*pos += ui_element_state_size(element->type);
// detailed element data e.g. UIInput
memcpy(element->details[0], *pos, ui_element_type_size(element->type));
*pos += ui_element_type_size(element->type);
}
// The size of layout->data should be the file size + a bunch of additional data for additional theme dependent "UIElements->details".
// Yes, this means we have a little too much data but not by a lot
int32 layout_from_data(
const byte* data,
UILayout* layout
) {
const byte* pos = data;
const byte* max_pos = pos;
int32 version = *((int32 *) pos);
pos += sizeof(version);
// Prepare hashmap (incl. reserve memory) by initializing it the same way we originally did
// Of course we still need to populate the data using hashmap_load()
// The value is a int64 (because this is the value of the chunk buffer size but the hashmap only allows int32)
hashmap_create(&layout->hash_map, (int32) SWAP_ENDIAN_LITTLE(*((uint64 *) pos)), sizeof(HashEntryInt64), layout->data);
const byte* start = data;
pos += hashmap_load(&layout->hash_map, pos);
// layout data
for (int32 i = 0; i < layout->hash_map.buf.count; ++i) {
if (!layout->hash_map.table[i]) {
continue;
}
HashEntryInt64* entry = (HashEntryInt64 *) layout->hash_map.table[i];
pos = start + entry->value;
ui_layout_parse_element(entry, layout->data, &pos);
if (pos > max_pos) {
max_pos = pos;
}
// save all the next elements
while (entry->next) {
pos = start + entry->value;
ui_layout_parse_element(entry, layout->data, &pos);
if (pos > max_pos) {
max_pos = pos;
}
entry = entry->next;
}
}
layout->layout_size = (uint32) (max_pos - data);
return (int32) layout->layout_size;
}
// @performance Implement a way to only load a specific element and all its children
// This way we can re-load specific elements on change and we could also greatly reduce the setup time by ignoring ui elements that are rarely visible
void layout_from_theme(
UILayout* layout,
const UIThemeStyle* theme,
const Camera* camera
) {
EvaluatorVariable variables[] = {
{ "vw", (f32) camera->viewport_width },
{ "vh", (f32) camera->viewport_height },
{ "px", 0.0 }, // Placeholder for parent values
{ "py", 0.0 }, // Placeholder for parent values
{ "pw", 0.0 }, // Placeholder for parent values
{ "ph", 0.0 }, // Placeholder for parent values
};
// Current position where we can the different sub elements (e.g. :hover, :active, ...)
byte* dynamic_pos = layout->data + layout->layout_size;
// We first need to handle the default element -> iterate all elements but only handle the default style
// @todo do here
for (int32 i = 0; i < theme->hash_map.buf.count; ++i) {
if (!theme->hash_map.table[i]) {
continue;
}
HashEntryUIntPtr* style_entry = (HashEntryUIntPtr *) theme->hash_map.table[i];
// We don't handle special styles here, only the default one
if (strchr(style_entry->key, ':')) {
continue;
}
HashEntry* entry = hashmap_get_entry(&layout->hash_map, style_entry->key);
if (!entry) {
// Couldn't even find the base element
continue;
}
// Populate default element
UIElement* element = (UIElement *) entry->value;
UIAttributeGroup* group = (UIAttributeGroup *) style_entry->value;
switch (element->type) {
case UI_ELEMENT_TYPE_INPUT: {
ui_input_state_populate(group, (UIInputState *) element->state);
ui_input_element_populate(group, element, UI_STYLE_TYPE_DEFAULT, variables);
} break;
}
}
// We iterate every style
// 1. Fill default element if it is default style
// 2. Create and fill new element if it isn't default style (e.g. :hover)
for (int32 i = 0; i < theme->hash_map.buf.count; ++i) {
if (!theme->hash_map.table[i]) {
continue;
}
HashEntryUIntPtr* style_entry = (HashEntryUIntPtr *) theme->hash_map.table[i];
// We only handle special styles here, not the default one
char* special = strchr(style_entry->key, ':');
if (!special) {
// The default element was already handled outside this loop
continue;
}
UIStyleType style_type = (UIStyleType) ui_style_type_to_id(special);
char pure_name[HASH_MAP_MAX_KEY_LENGTH];
str_copy_until(style_entry->key, pure_name, ':');
HashEntry* entry = hashmap_get_entry(&layout->hash_map, pure_name);
if (!entry) {
// Couldn't even find the base element
continue;
}
UIElement* element = (UIElement *) entry->value;
// Doesn't exist (usually the first load, but exists when we resize our window)
if (!element->details[style_type]) {
element->details[style_type] = dynamic_pos;
dynamic_pos += ui_element_type_size(element->type);
}
// The style inherits from the default element
memcpy(element->details[style_type], element->details[0], ui_element_type_size(element->type));
// Populate element details
UIAttributeGroup* group = (UIAttributeGroup *) style_entry->value;
switch (element->type) {
case UI_ELEMENT_TYPE_INPUT: {
ui_input_element_populate(group, element, style_type, variables);
} break;
}
}
}
inline
uint32 layout_element_from_location(UILayout* layout, uint16 x, uint16 y)
{
return layout->ui_chroma_codes[layout->width * y / 4 + x / 4];
}
#endif