cOMS/ui/UITheme.h
Dennis Eichhorn eb9a135ca7
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (autobuild, c-cpp) (push) Has been cancelled
Microsoft C++ Code Analysis / Analyze (push) Has been cancelled
fixing bugs and adding some test scripts
2025-04-27 20:10:58 +00:00

393 lines
14 KiB
C
Executable File

#ifndef COMS_UI_THEME_H
#define COMS_UI_THEME_H
#include "../stdlib/Types.h"
#include "../memory/RingMemory.h"
#include "../utils/EndianUtils.h"
#include "../utils/StringUtils.h"
#include "../stdlib/HashMap.h"
#include "../font/Font.h"
#include "../system/FileUtils.cpp"
#include "../sort/Sort.h"
#include "attribute/UIAttribute.h"
#include "UIElementType.h"
#define UI_THEME_VERSION 1
// @todo We should add some asserts that ensure that the respective structs at least start at a 4byte memory alignment
// @question Currently there is some data duplication in here and in the UIElement.
// Not sure if this is how we want this to be or if we want to change this in the future
// Modified for every scene
// WARNING: Make sure the order of this struct and UITheme is the same for the first elements
// This allows us to cast between both
struct UIThemeStyle {
// A theme may have N named styles
// The hashmap contains the offset where the respective style can be found
// @performance Switch to perfect hash map
HashMap hash_map;
uint32 data_size;
byte* data;
// @question It feels weird that this is here, especially considering we could have multiple fonts
// Maybe we should have an array of fonts (e.g. allow up to 3 fonts per theme?)
Font* font;
};
inline
UIAttributeGroup* theme_style_group(UIThemeStyle* theme, const char* group_name)
{
HashEntryInt32* entry = (HashEntryInt32 *) hashmap_get_entry(&theme->hash_map, group_name);
if (!entry) {
ASSERT_SIMPLE(false);
return NULL;
}
return (UIAttributeGroup *) (theme->data + entry->value);
}
static inline
int compare_by_attribute_id(const void* __restrict a, const void* __restrict b) noexcept {
UIAttribute* attr_a = (UIAttribute *) a;
UIAttribute* attr_b = (UIAttribute *) b;
return attr_a->attribute_id - attr_b->attribute_id;
}
// File layout - text
// version
// #group_name
// attributes ...
// attributes ...
// attributes ...
// #group_name
// attributes ...
// attributes ...
// attributes ...
// WARNING: theme needs to have memory already reserved and assigned to data
void theme_from_file_txt(
UIThemeStyle* theme,
const char* path,
RingMemory* ring
) {
FileBody file = {};
file_read(path, &file, ring);
ASSERT_SIMPLE(file.size);
const char* pos = (char *) file.content;
// move past the "version" string
pos += 8;
// Use version for different handling
[[maybe_unused]] int32 version = (int32) str_to_int(pos, &pos); ++pos;
// We have to find how many groups are defined in the theme file.
// Therefore we have to do an initial iteration
int32 temp_group_count = 0;
while (*pos != '\0') {
// Skip all white spaces
str_skip_empty(&pos);
// Is group name
if (*pos == '#' || *pos == '.') {
++temp_group_count;
}
// Go to the next line
str_move_past(&pos, '\n');
}
// @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(&theme->hash_map, temp_group_count, sizeof(HashEntryInt32), theme->data);
int32 data_offset = (int32) hashmap_size(&theme->hash_map);
UIAttributeGroup* temp_group = NULL;
pos = (char *) file.content;
// move past "version" string
str_move_past(&pos, '\n');
char attribute_name[32];
char block_name[HASH_MAP_MAX_KEY_LENGTH];
bool last_token_newline = false;
bool block_open = false;
while (*pos != '\0') {
str_skip_whitespace(&pos);
if (is_eol(pos)) {
pos += is_eol(pos);
// 2 new lines => closing block
if (last_token_newline) {
block_open = false;
}
last_token_newline = !last_token_newline;
continue;
}
last_token_newline = false;
// Handle new group
/////////////////////////////////////////////////////////
if (!block_open) {
str_copy_move_until(block_name, &pos, " \r\n");
// All blocks need to start with #. In the past this wasn't the case and may not be in the future. This is why we keep this if here.
if (*block_name == '#' || *block_name == '.') {
// Named style
block_open = true;
if (temp_group) {
// Before we insert a new group we have to sort the attributes of the PREVIOUS temp_group
// since this makes searching them later on more efficient.
sort_introsort(temp_group + 1, temp_group->attribute_count, sizeof(UIAttribute), compare_by_attribute_id);
// @todo This is where we create the Eytzinger order if we want to use it
}
// Insert new group
hashmap_insert(&theme->hash_map, block_name, data_offset);
temp_group = (UIAttributeGroup *) (theme->data + data_offset);
temp_group->attribute_count = 0;
data_offset += sizeof(UIAttributeGroup);
}
continue;
}
// Handle new attribute for previously found group
/////////////////////////////////////////////////////////
str_copy_move_until(attribute_name, &pos, " :\n");
// Skip any white spaces or other delimeter
str_skip_list(&pos, " \t:", sizeof(" \t:") - 1);
// Parse attribute value
UIAttribute attribute = {};
ui_attribute_parse_value(&attribute, attribute_name, pos);
// @bug How to handle "anim"
// We currently just end the block if we encounter it
if (attribute.attribute_id == UI_ATTRIBUTE_TYPE_ANIMATION) {
block_open = false;
continue;
}
// Again, currently this if check is redundant but it wasn't in the past and we may need it again in the future.
if (block_name[0] == '#' || block_name[0] == '.') {
// Named block
UIAttribute* attribute_reference = (UIAttribute *) (temp_group + 1);
// @question Whe are we even doing this? couldn't we just pass this offset to the ui_attribute_parse_value() function?
memcpy(
attribute_reference + temp_group->attribute_count,
&attribute,
sizeof(attribute)
);
data_offset += sizeof(attribute);
++temp_group->attribute_count;
}
str_move_to(&pos, '\n');
}
// We still need to sort the last group
sort_introsort(temp_group + 1, temp_group->attribute_count, sizeof(UIAttribute), compare_by_attribute_id);
// @todo This is where we create the Eytzinger order if we want to use it
}
static inline
void ui_theme_parse_group(HashEntryInt32* entry, byte* data, const byte** in)
{
// @performance Are we sure the data is nicely aligned?
// Probably depends on the from_txt function and the start of theme->data
UIAttributeGroup* group = (UIAttributeGroup *) (data + entry->value);
group->attribute_count = SWAP_ENDIAN_LITTLE(*((uint32 *) *in));
*in += sizeof(group->attribute_count);
UIAttribute* attribute_reference = (UIAttribute *) (group + 1);
for (uint32 j = 0; j < group->attribute_count; ++j) {
attribute_reference[j].attribute_id = (UIAttributeType) SWAP_ENDIAN_LITTLE(*((uint16 *) *in));
*in += sizeof(attribute_reference[j].attribute_id);
attribute_reference[j].datatype = *((UIAttributeDataType *) *in);
*in += sizeof(attribute_reference[j].datatype);
if (attribute_reference[j].datatype == UI_ATTRIBUTE_DATA_TYPE_INT) {
attribute_reference[j].value_int = SWAP_ENDIAN_LITTLE(*((int32 *) *in));
*in += sizeof(attribute_reference[j].value_int);
} else if (attribute_reference[j].datatype == UI_ATTRIBUTE_DATA_TYPE_INT) {
attribute_reference[j].value_float = SWAP_ENDIAN_LITTLE(*((f32 *) *in));
*in += sizeof(attribute_reference[j].value_float);
} else if (attribute_reference[j].datatype == UI_ATTRIBUTE_DATA_TYPE_STR) {
memcpy(attribute_reference[j].value_str, *in, sizeof(attribute_reference[j].value_str));
*in += sizeof(attribute_reference[j].value_str);
} else if (attribute_reference[j].datatype == UI_ATTRIBUTE_DATA_TYPE_V4_F32) {
attribute_reference[j].value_v4_f32.x = SWAP_ENDIAN_LITTLE(*((f32 *) *in));
*in += sizeof(attribute_reference[j].value_v4_f32.x);
attribute_reference[j].value_v4_f32.y = SWAP_ENDIAN_LITTLE(*((f32 *) *in));
*in += sizeof(attribute_reference[j].value_v4_f32.y);
attribute_reference[j].value_v4_f32.z = SWAP_ENDIAN_LITTLE(*((f32 *) *in));
*in += sizeof(attribute_reference[j].value_v4_f32.z);
attribute_reference[j].value_v4_f32.w = SWAP_ENDIAN_LITTLE(*((f32 *) *in));
*in += sizeof(attribute_reference[j].value_v4_f32.w);
}
}
}
// Memory layout (data) - This is not the layout of the file itself, just how we represent it in memory
// Hashmap
// UIAttributeGroup - This is where the pointers point to (or what the offset represents)
// size
// Attributes ...
// Attributes ...
// Attributes ...
// UIAttributeGroup
// size
// Attributes ...
// Attributes ...
// Attributes ...
// The size of theme->data should be the file size.
// Yes, this means we have a little too much data but not by a lot
int32 theme_from_data(
const byte* __restrict data,
UIThemeStyle* __restrict theme
) {
PROFILE(PROFILE_THEME_FROM_THEME, NULL, false, true);
LOG_1("Load theme");
const byte* in = data;
int32 version = SWAP_ENDIAN_LITTLE(*((int32 *) in));
in += 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(&theme->hash_map, (int32) SWAP_ENDIAN_LITTLE(*((uint32 *) in)), sizeof(HashEntryInt32), theme->data);
in += hashmap_load(&theme->hash_map, in);
// theme data
// Layout: first load the size of the group, then load the individual attributes
// @performance We are iterating the hashmap twice (hashmap_load and here)
uint32 chunk_id = 0;
chunk_iterate_start(&theme->hash_map.buf, chunk_id) {
HashEntryInt32* entry = (HashEntryInt32 *) chunk_get_element((ChunkMemory *) &theme->hash_map.buf, chunk_id);
ui_theme_parse_group(entry, theme->data, &in);
} chunk_iterate_end;
LOG_1("Loaded theme");
return (int32) (in - data);
}
// Calculates the maximum theme size
// Not every group has all the attributes (most likely only a small subset)
// However, an accurate calculation is probably too slow and not needed most of the time
inline
int64 theme_data_size_max(const UIThemeStyle* theme)
{
return hashmap_size(&theme->hash_map)
+ theme->hash_map.buf.count * UI_ATTRIBUTE_TYPE_SIZE * sizeof(UIAttribute);
}
// @todo Why do even need **pos, shouldn't it just be *pos
static inline
void ui_theme_serialize_group(const HashEntryInt32* entry, byte* data, byte** out)
{
// @performance Are we sure the data is nicely aligned?
// Probably depends on the from_txt function and the start of theme->data
UIAttributeGroup* group = (UIAttributeGroup *) (data + entry->value);
*((uint32 *) *out) = SWAP_ENDIAN_LITTLE(group->attribute_count);
*out += sizeof(group->attribute_count);
UIAttribute* attribute_reference = (UIAttribute *) (group + 1);
for (uint32 j = 0; j < group->attribute_count; ++j) {
*((uint16 *) *out) = SWAP_ENDIAN_LITTLE(attribute_reference[j].attribute_id);
*out += sizeof(attribute_reference[j].attribute_id);
*((byte *) *out) = attribute_reference[j].datatype;
*out += sizeof(attribute_reference[j].datatype);
if (attribute_reference[j].datatype == UI_ATTRIBUTE_DATA_TYPE_INT) {
*((int32 *) *out) = SWAP_ENDIAN_LITTLE(attribute_reference[j].value_int);
*out += sizeof(attribute_reference[j].value_int);
} else if (attribute_reference[j].datatype == UI_ATTRIBUTE_DATA_TYPE_INT) {
*((f32 *) *out) = SWAP_ENDIAN_LITTLE(attribute_reference[j].value_float);
*out += sizeof(attribute_reference[j].value_float);
} else if (attribute_reference[j].datatype == UI_ATTRIBUTE_DATA_TYPE_STR) {
memcpy(*out, attribute_reference[j].value_str, sizeof(attribute_reference[j].value_str));
*out += sizeof(attribute_reference[j].value_str);
} else if (attribute_reference[j].datatype == UI_ATTRIBUTE_DATA_TYPE_V4_F32) {
*((f32 *) *out) = SWAP_ENDIAN_LITTLE(attribute_reference[j].value_v4_f32.x);
*out += sizeof(attribute_reference[j].value_v4_f32.x);
*((f32 *) *out) = SWAP_ENDIAN_LITTLE(attribute_reference[j].value_v4_f32.y);
*out += sizeof(attribute_reference[j].value_v4_f32.y);
*((f32 *) *out) = SWAP_ENDIAN_LITTLE(attribute_reference[j].value_v4_f32.z);
*out += sizeof(attribute_reference[j].value_v4_f32.z);
*((f32 *) *out) = SWAP_ENDIAN_LITTLE(attribute_reference[j].value_v4_f32.w);
*out += sizeof(attribute_reference[j].value_v4_f32.w);
}
}
}
// File layout - binary
// version
// hashmap size
// Hashmap (includes offsets to the individual groups)
// #group_name (this line doesn't really exist in file, it's more like the pointer offset in the hashmap)
// size
// attributes ...
// attributes ...
// attributes ...
// #group_name
// size
// attributes ...
// attributes ...
// attributes ...
int32 theme_to_data(
const UIThemeStyle* __restrict theme,
byte* __restrict data
) {
byte* out = data;
// version
*((int32 *) out) = SWAP_ENDIAN_LITTLE(UI_THEME_VERSION);
out += sizeof(int32);
// hashmap
out += hashmap_dump(&theme->hash_map, out);
// theme data
// Layout: first save the size of the group, then save the individual attributes
uint32 chunk_id = 0;
chunk_iterate_start(&theme->hash_map.buf, chunk_id) {
const HashEntryInt32* entry = (HashEntryInt32 *) chunk_get_element((ChunkMemory *) &theme->hash_map.buf, chunk_id);
ui_theme_serialize_group(entry, theme->data, &out);
} chunk_iterate_end;
return (int32) (out - data);
}
#endif