#ifndef TOS_UI_THEME_H #define TOS_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 "UIAttribute.h" #include "UIElementType.h" #if _WIN32 #include "../platform/win32/UtilsWin32.h" #else #include "../platform/linux/UtilsLinux.h" #endif #define UI_THEME_VERSION 1 // @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 { byte* data; int32 version; // A theme may have N named styles // The hashmap contains the offset where the respective style can be found HashMap hash_map; }; // General theme for all scenes struct UITheme { // This one usually remains unchanged unless someone changes the theme UIThemeStyle ui_general; // This one is scene specific and is loaded with the scene // We have a pointer that references the currently active scene // The other two elements contain the actual data. // This allows us to easily pre-fetch scene styles and the pointer allows us to easily switch between them // When loading a new scene we simply use the style that is currently not pointed to by primary_scene UIThemeStyle* primary_scene; UIThemeStyle ui_scene1; UIThemeStyle ui_scene2; // @question This basically means we only support 1 font for the UI ?! // This is probably something to re-consider Font font; // @todo add cursor styles // @todo what about ui audio? char name[32]; }; // @performance Consider to replace HashEntryInt64 with HashEntryVoidP. // This way we wouldn't have to do theme->data + entry->value and could just use entry->value // Of course this means during the saving and loading we need to convert to and from offsets // The problem is that the actual dumping and loading for that part doesn't happen in the hashmap but in the chunk_memory // The chunk_memory doesn't know how the value look like -> cannot do the conversion inline UIAttributeGroup* theme_style_group(UIThemeStyle* theme, const char* group_name) { HashEntryInt64* entry = (HashEntryInt64 *) hashmap_get_entry(&theme->hash_map, group_name); if (!entry) { ASSERT_SIMPLE(false); return NULL; } return (UIAttributeGroup *) (theme->data + entry->value); } inline UIAttributeGroup* theme_style_group(UIThemeStyle* theme, const char* group_name, int32 group_id) { HashEntryInt64* entry = (HashEntryInt64 *) hashmap_get_entry(&theme->hash_map, group_name, group_id); if (!entry) { ASSERT_SIMPLE(false); return NULL; } return (UIAttributeGroup *) (theme->data + entry->value); } int compare_by_attribute_id(const void* a, const void* b) { 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, byte* data ) { char* pos = (char *) data; // move past the version string pos += 8; theme->version = strtol(pos, &pos, 10); ++pos; bool block_open = false; char block_name[32]; char attribute_name[32]; bool last_token_newline = false; // 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 end of the line str_move_to(&pos, '\n'); // Go to next line if (*pos != '\0') { ++pos; } } // @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(HashEntryInt64), theme->data); int64 data_offset = hashmap_size(&theme->hash_map); UIAttributeGroup* temp_group = NULL; pos = (char *) data; pos += 8; // move past version while (*pos != '\0') { str_skip_empty(&pos); if (*pos == '\n') { ++pos; // 2 new lines => closing block if (last_token_newline) { block_open = false; last_token_newline = false; } else { last_token_newline = true; } continue; } last_token_newline = false; if (!block_open) { str_copy_move_until(&pos, block_name, " \n", sizeof(" \n") - 1); // 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 // since this makes searching them later on more efficient. qsort(temp_group->attributes, temp_group->attribute_size, sizeof(UIAttribute), compare_by_attribute_id); } // Insert new group hashmap_insert(&theme->hash_map, block_name, data_offset); temp_group = (UIAttributeGroup *) (theme->data + data_offset); temp_group->attribute_size = 0; temp_group->attributes = (UIAttribute *) (theme->data + data_offset + sizeof(UIAttributeGroup)); data_offset += sizeof(UIAttributeGroup); } continue; } str_copy_move_until(&pos, attribute_name, " :\n", sizeof(" :\n") - 1); // Skip any white spaces or other delimeters str_skip_list(&pos, " \t:", sizeof(" \t:") - 1); ASSERT_SIMPLE((*pos != '\0' && *pos != '\n')); // Handle different attribute types UIAttribute attribute = {}; if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_TYPE), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_TYPE; char str[32]; str_copy_move_until(&pos, str, '\n'); for (int32 j = 0; j < UI_ELEMENT_TYPE_SIZE; ++j) { if (strcmp(str, ui_element_type_to_string_const((UIElementType) j)) == 0) { attribute.value_int = j; break; } } ++pos; } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_STYLE), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_STYLE; str_copy_move_until(&pos, attribute.value_str, '\n'); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_FONT_COLOR), attribute_name) == 0) { ++pos; // Skip '#' attribute.attribute_id = UI_ATTRIBUTE_TYPE_FONT_COLOR; hexstr_to_rgba(&attribute.value_v4_f32, pos); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_FONT_SIZE), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_FONT_SIZE; attribute.value_float = strtof(pos, &pos); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_FONT_WEIGHT), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_FONT_WEIGHT; attribute.value_int = strtoul(pos, &pos, 10); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_FONT_LINE_HEIGHT), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_FONT_LINE_HEIGHT; attribute.value_float = strtof(pos, &pos); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_ALIGN_H), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_ALIGN_H; attribute.value_int = strtoul(pos, &pos, 10); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_ALIGN_V), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_ALIGN_V; attribute.value_int = strtoul(pos, &pos, 10); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_ZINDEX), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_ZINDEX; attribute.value_float = SWAP_ENDIAN_LITTLE(strtof(pos, &pos)); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_BACKGROUND_COLOR), attribute_name) == 0) { ++pos; // Skip '#' attribute.attribute_id = UI_ATTRIBUTE_TYPE_BACKGROUND_COLOR; hexstr_to_rgba(&attribute.value_v4_f32, pos); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_BACKGROUND_IMG), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_BACKGROUND_IMG; str_copy_move_until(&pos, attribute.value_str, '\n'); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_BACKGROUND_IMG_OPACITY), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_BACKGROUND_IMG_OPACITY; attribute.value_float = SWAP_ENDIAN_LITTLE(strtof(pos, &pos)); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_BACKGROUND_IMG_POSITION_V), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_BACKGROUND_IMG_POSITION_V; attribute.value_int = strtoul(pos, &pos, 10); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_BACKGROUND_IMG_POSITION_H), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_BACKGROUND_IMG_POSITION_H; attribute.value_int = strtoul(pos, &pos, 10); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_BACKGROUND_IMG_STYLE), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_BACKGROUND_IMG_STYLE; attribute.value_int = strtoul(pos, &pos, 10); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_BORDER_COLOR), attribute_name) == 0) { ++pos; // Skip '#' attribute.attribute_id = UI_ATTRIBUTE_TYPE_BORDER_COLOR; hexstr_to_rgba(&attribute.value_v4_f32, pos); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_BORDER_WIDTH), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_BORDER_WIDTH; attribute.value_int = strtoul(pos, &pos, 10); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_BORDER_TOP_COLOR), attribute_name) == 0) { ++pos; // Skip '#' attribute.attribute_id = UI_ATTRIBUTE_TYPE_BORDER_TOP_COLOR; hexstr_to_rgba(&attribute.value_v4_f32, pos); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_BORDER_TOP_WIDTH), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_BORDER_TOP_WIDTH; attribute.value_int = strtoul(pos, &pos, 10); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_BORDER_RIGHT_COLOR), attribute_name) == 0) { ++pos; // Skip '#' attribute.attribute_id = UI_ATTRIBUTE_TYPE_BORDER_RIGHT_COLOR; hexstr_to_rgba(&attribute.value_v4_f32, pos); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_BORDER_RIGHT_WIDTH), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_BORDER_RIGHT_WIDTH; attribute.value_int = strtoul(pos, &pos, 10); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_BORDER_BOTTOM_COLOR), attribute_name) == 0) { ++pos; // Skip '#' attribute.attribute_id = UI_ATTRIBUTE_TYPE_BORDER_BOTTOM_COLOR; hexstr_to_rgba(&attribute.value_v4_f32, pos); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_BORDER_BOTTOM_WIDTH), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_BORDER_BOTTOM_WIDTH; attribute.value_int = strtoul(pos, &pos, 10); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_BORDER_LEFT_COLOR), attribute_name) == 0) { ++pos; // Skip '#' attribute.attribute_id = UI_ATTRIBUTE_TYPE_BORDER_LEFT_COLOR; hexstr_to_rgba(&attribute.value_v4_f32, pos); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_BORDER_LEFT_WIDTH), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_BORDER_LEFT_WIDTH; attribute.value_int = strtoul(pos, &pos, 10); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_PADDING), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_PADDING; attribute.value_int = strtoul(pos, &pos, 10); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_PADDING_TOP), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_PADDING_TOP; attribute.value_int = strtoul(pos, &pos, 10); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_PADDING_RIGHT), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_PADDING_RIGHT; attribute.value_int = strtoul(pos, &pos, 10); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_PADDING_BOTTOM), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_PADDING_BOTTOM; attribute.value_int = strtoul(pos, &pos, 10); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_PADDING_LEFT), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_PADDING_LEFT; attribute.value_int = strtoul(pos, &pos, 10); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_SHADOW_INNER_COLOR), attribute_name) == 0) { ++pos; // Skip '#' attribute.attribute_id = UI_ATTRIBUTE_TYPE_SHADOW_INNER_COLOR; hexstr_to_rgba(&attribute.value_v4_f32, pos); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_SHADOW_INNER_ANGLE), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_SHADOW_INNER_ANGLE; attribute.value_float = strtof(pos, &pos); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_SHADOW_INNER_DISTANCE), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_SHADOW_INNER_DISTANCE; attribute.value_int = strtoul(pos, &pos, 10); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_SHADOW_OUTER_COLOR), attribute_name) == 0) { ++pos; // Skip '#' attribute.attribute_id = UI_ATTRIBUTE_TYPE_SHADOW_OUTER_COLOR; hexstr_to_rgba(&attribute.value_v4_f32, pos); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_SHADOW_OUTER_ANGLE), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_SHADOW_OUTER_ANGLE; attribute.value_float = strtof(pos, &pos); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_SHADOW_OUTER_DISTANCE), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_SHADOW_OUTER_DISTANCE; attribute.value_int = strtoul(pos, &pos, 10); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_TRANSITION_ANIMATION), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_TRANSITION_ANIMATION; attribute.value_int = strtoul(pos, &pos, 10); } else if (strcmp(ui_attribute_type_to_string_const(UI_ATTRIBUTE_TYPE_TRANSITION_DURATION), attribute_name) == 0) { attribute.attribute_id = UI_ATTRIBUTE_TYPE_TRANSITION_DURATION; attribute.value_float = strtof(pos, &pos); } else { str_move_to(&pos, '\n'); 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 memcpy( temp_group->attributes + temp_group->attribute_size, &attribute, sizeof(attribute) ); data_offset += sizeof(attribute); ++temp_group->attribute_size; } str_move_to(&pos, '\n'); } // We still need to sort the last group qsort(temp_group->attributes, temp_group->attribute_size, sizeof(UIAttribute), compare_by_attribute_id); } // 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 void theme_from_file( UIThemeStyle* theme, const byte* data ) { const byte* pos = data; theme->version = *((int32 *) pos); pos += sizeof(theme->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(*((uint64 *) pos)), sizeof(HashEntryInt64), theme->data); const byte* start = theme->hash_map.buf.memory; pos += hashmap_load(&theme->hash_map, pos); // theme data // Layout: first load the size of the group, then load the individual attributes for (int32 i = 0; i < theme->hash_map.buf.count; ++i) { if (!theme->hash_map.table[i]) { continue; } HashEntryInt64* entry = (HashEntryInt64 *) theme->hash_map.table[i]; pos = start + entry->value; UIAttributeGroup* group = (UIAttributeGroup *) (theme->data + entry->value); group->attribute_size = SWAP_ENDIAN_LITTLE(*((int32 *) pos)); pos += sizeof(group->attribute_size); // @performance The UIAttribute contains a char array which makes this WAY larger than it needs to be in 99% of the cases memcpy(group->attributes, pos, group->attribute_size * sizeof(UIAttribute)); pos += group->attribute_size * sizeof(UIAttribute); // load all the next elements while (entry->next) { pos = start + entry->value; group = (UIAttributeGroup *) (theme->data + entry->value); group->attribute_size = SWAP_ENDIAN_LITTLE(*((int32 *) pos)); pos += sizeof(group->attribute_size); // @performance The UIAttribute contains a char array which makes this WAY larger than it needs to be in 99% of the cases memcpy(group->attributes, pos, group->attribute_size * sizeof(UIAttribute)); pos += group->attribute_size * sizeof(UIAttribute); entry = entry->next; } } } // 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_size(const UIThemeStyle* theme) { return hashmap_size(&theme->hash_map) + theme->hash_map.buf.count * UI_ATTRIBUTE_TYPE_SIZE * sizeof(UIAttribute); } // 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 ... void theme_to_file( RingMemory* ring, const char* path, const UIThemeStyle* theme ) { FileBody file; // Temporary file size for buffer // @todo This is a bad placeholder, The problem is we don't know how much we actually need without stepping through the elements // I also don't want to add a size variable to the theme as it is useless in all other cases file.size = theme_size(theme); file.content = ring_get_memory(ring, file.size, 64, true); byte* pos = file.content; // version *((int32 *) pos) = SWAP_ENDIAN_LITTLE(theme->version); pos += sizeof(theme->version); // hashmap byte* start = pos; pos += hashmap_dump(&theme->hash_map, pos); // theme data // Layout: first save the size of the group, then save the individual attributes for (int32 i = 0; i < theme->hash_map.buf.count; ++i) { if (!theme->hash_map.table[i]) { continue; } HashEntryInt64* entry = (HashEntryInt64 *) theme->hash_map.table[i]; pos = start + entry->value; UIAttributeGroup* group = (UIAttributeGroup *) (theme->data + entry->value); *((int32 *) pos) = SWAP_ENDIAN_LITTLE(group->attribute_size); pos += sizeof(group->attribute_size); // @performance The UIAttribute contains a char array which makes this WAY larger than it needs to be in 99% of the cases memcpy(pos, group->attributes, group->attribute_size * sizeof(UIAttribute)); pos += sizeof(UIAttribute); // save all the next elements while (entry->next) { pos = start + entry->value; group = (UIAttributeGroup *) (theme->data + entry->value); *((int32 *) pos) = SWAP_ENDIAN_LITTLE(group->attribute_size); pos += sizeof(group->attribute_size); // @performance The UIAttribute contains a char array which makes this WAY larger than it needs to be in 99% of the cases memcpy(pos, group->attributes, group->attribute_size * sizeof(UIAttribute)); pos += sizeof(UIAttribute); entry = entry->next; } } file.size = pos - file.content; file_write(path, &file); } #endif