From 44e987c8521a99519350a42292bcfcd28451dcbd Mon Sep 17 00:00:00 2001 From: dec05eba Date: Mon, 20 Dec 2021 10:26:12 +0100 Subject: Async load images --- TODO | 8 +- depends/mgl | 2 +- include/async_image.h | 45 ++++++++ include/hashmap.h | 4 + include/mgui/image.h | 5 + include/mgui/list.h | 2 + include/mgui/mgui.h | 4 + include/mgui/richtext.h | 1 + include/mgui/widget.h | 9 +- src/async_image.c | 297 ++++++++++++++++++++++++++++++++++++++++++++++++ src/hashmap.c | 47 +++++++- src/mgui/button.c | 4 + src/mgui/image.c | 38 +++++-- src/mgui/label.c | 4 + src/mgui/list.c | 32 +++++- src/mgui/mgui.c | 24 +++- src/mgui/richtext.c | 26 +++-- src/mgui/scrollview.c | 42 +++++-- src/mgui/widget.c | 21 ++-- tests/main.c | 23 ++-- 20 files changed, 584 insertions(+), 54 deletions(-) create mode 100644 include/async_image.h create mode 100644 src/async_image.c diff --git a/TODO b/TODO index a898137..a4d3437 100644 --- a/TODO +++ b/TODO @@ -3,4 +3,10 @@ Widget horizontal/vertical expand. Homogeneous sizing for list items. Allow setting list direction. This is more efficient than prepending widgets to a list. Stop rendering scroll view when reaching outside the scissor view. -Widgets such as richtext and label should unload their text when updated but not rendered, this allows the widget to save its render size without saving the render data. \ No newline at end of file +Widgets such as richtext and label should unload their text when updated but not rendered, this allows the widget to save its render size without saving the render data. +If a richtext widget has too little horizontal space and does wordwrap then disable wordwrap and use horizontal scrolling. +Cut widget collision check by scissor. +Handle scrolling when resizing scroll content and widget items change size (images downscaled, richtext wrapping, etc). +Cut event and size calculation for widgets outside scissor. +Calculate text (richtext and label) height better when its a single line. In such cases it should be the glyph height and not character size. +Move richtext vertices to global space and only add vertices for the visible richtext. \ No newline at end of file diff --git a/depends/mgl b/depends/mgl index 4537803..347a6fa 160000 --- a/depends/mgl +++ b/depends/mgl @@ -1 +1 @@ -Subproject commit 4537803743e51ecb01925e9c5b4bef44aad8386b +Subproject commit 347a6fa2795a6856c1ad3d623673b62f879227c1 diff --git a/include/async_image.h b/include/async_image.h new file mode 100644 index 0000000..a108d5e --- /dev/null +++ b/include/async_image.h @@ -0,0 +1,45 @@ +#ifndef MGUI_ASYNC_IMAGE_H +#define MGUI_ASYNC_IMAGE_H + +#include +#include +#include + +typedef struct mgui_async_image mgui_async_image; + +typedef enum { + MGUI_ASYNC_IMAGE_NOT_LOADED, + MGUI_ASYNC_IMAGE_LOADING, + MGUI_ASYNC_IMAGE_LOADED, + MGUI_ASYNC_IMAGE_APPLIED, + MGUI_ASYNC_IMAGE_FAILED_TO_LOAD, + MGUI_ASYNC_IMAGE_UNLOADED +} mgui_async_image_state; + +struct mgui_async_image { + mgl_image image; + mgl_texture texture; + uint64_t hash; + mgui_async_image_state state; + uint32_t updated; + uint32_t ref_count; + char *filepath; +}; + +/* Do not call this manually */ +void mgui_async_image_init(); +/* Do not call this manually */ +void mgui_async_image_deinit(); +/* Do not call this manually */ +void mgui_async_image_unload_unreferenced(); + +/* + Increases reference count by 1 every time this is called. + Call |mgui_async_image_unref| when done with the image so it can be unloaded when all references are gone. +*/ +mgui_async_image* mgui_async_image_get_by_path(const char *filepath); +/* Needs to be called at least once a frame to keep the image from being unloaded */ +void mgui_async_image_update(mgui_async_image *self); +void mgui_async_image_unref(mgui_async_image *self); + +#endif /* MGUI_ASYNC_IMAGE_H */ diff --git a/include/hashmap.h b/include/hashmap.h index 0928081..f027d25 100644 --- a/include/hashmap.h +++ b/include/hashmap.h @@ -34,5 +34,9 @@ bool mgui_hashmap_get(mgui_hashmap *self, const char *key, size_t key_size, void /* Note: |hash| has to be the hash for |key|, which you can get by using calling |mgui_hashmap_hash| with |key| or as a result of |mgui_hashmap_insert| */ bool mgui_hashmap_get_by_hash(mgui_hashmap *self, const char *key, size_t key_size, uint64_t hash, void **value_out); uint64_t mgui_hashmap_hash(const char *key, size_t key_size); +/* Return false from |callback| to stop the iteration */ +void mgui_hashmap_for_each(mgui_hashmap *self, bool(*callback)(void *value, void *userdata), void *userdata); +/* Return true from |callback| to erase the item */ +void mgui_hashmap_for_each_erase(mgui_hashmap *self, bool(*callback)(void *value, void *userdata), void *userdata); #endif /* MGUI_HASHMAP_H */ diff --git a/include/mgui/image.h b/include/mgui/image.h index e6787f2..d9ed025 100644 --- a/include/mgui/image.h +++ b/include/mgui/image.h @@ -4,11 +4,16 @@ #include "widget.h" #include +typedef struct mgui_async_image mgui_async_image; + typedef struct { mgui_widget widget; mgl_sprite sprite; + mgui_async_image *async_image; + mgl_vec2i max_size; } mgui_image; +/* If |filepath| is not a valid image then the image will be a broken-icon type */ mgui_image* mgui_image_create(const char *filepath); void mgui_image_destroy(mgui_image *image); mgui_widget* mgui_image_to_widget(mgui_image *list); diff --git a/include/mgui/list.h b/include/mgui/list.h index 7769ab6..92dce0a 100644 --- a/include/mgui/list.h +++ b/include/mgui/list.h @@ -21,6 +21,7 @@ typedef struct { typedef struct { mgui_widget widget; mgui_list_direction direction; + int spacing; mgl_vec2i position; mgui_list_item *items; size_t items_capacity; @@ -32,6 +33,7 @@ void mgui_list_destroy(mgui_list *list); mgui_widget* mgui_list_to_widget(mgui_list *list); mgui_list* mgui_widget_to_list(mgui_widget *widget); +void mgui_list_set_spacing(mgui_list *self, int spacing); void mgui_list_set_position(mgui_list *self, mgl_vec2i position); void mgui_list_calculate_size(mgui_list *self, mgl_vec2i max_size); void mgui_list_append(mgui_list *self, mgui_widget *widget); diff --git a/include/mgui/mgui.h b/include/mgui/mgui.h index 15a2f73..cb018d5 100644 --- a/include/mgui/mgui.h +++ b/include/mgui/mgui.h @@ -6,9 +6,13 @@ typedef struct mgl_event mgl_event; typedef struct mgl_window mgl_window; void mgui_init(); +void mgui_deinit(); void mgui_on_event(mgui_widget *root_widget, mgl_window *window, mgl_event *event); +/* This should only be called once every frame */ void mgui_draw(mgui_widget *root_widget, mgl_window *window); /* Clamped to 1.0 second */ double mgui_get_seconds_since_last_update(); +/* Clamped to 1.0 second */ +double mgui_get_frame_time_seconds(); #endif /* MGUI_H */ diff --git a/include/mgui/richtext.h b/include/mgui/richtext.h index ff57271..f3b07a5 100644 --- a/include/mgui/richtext.h +++ b/include/mgui/richtext.h @@ -23,6 +23,7 @@ typedef struct { int width; mgui_richtext_vertex_data vertex_data[2]; bool dirty; + bool vertices_dirty; } mgui_richtext; mgui_richtext* mgui_richtext_create(const char *str, size_t size, unsigned char character_size); diff --git a/include/mgui/widget.h b/include/mgui/widget.h index d425c11..0e77fd2 100644 --- a/include/mgui/widget.h +++ b/include/mgui/widget.h @@ -40,10 +40,10 @@ typedef enum { } mgui_widget_flags; typedef struct { - int left; - int right; - int top; - int bottom; + uint16_t left; + uint16_t right; + uint16_t top; + uint16_t bottom; } mgui_margin; struct mgui_widget { @@ -52,6 +52,7 @@ struct mgui_widget { uint8_t alignment; /* mgui_alignment, MGUI_WIDGET_ALIGN_TOP_LEFT by default */ mgui_margin margin; mgl_vec2i size; + void *userdata; }; void mgui_widget_init(mgui_widget *self, mgui_widget_type type); diff --git a/src/async_image.c b/src/async_image.c new file mode 100644 index 0000000..4e509dc --- /dev/null +++ b/src/async_image.c @@ -0,0 +1,297 @@ +#include "../include/async_image.h" +#include "../include/alloc.h" +#include "../include/hashmap.h" +#include +#include +#include +#include +#include + +typedef struct { + mgui_async_image *async_image; +} async_image_task; + +typedef struct { + async_image_task *tasks; + size_t capacity; + size_t start_index; + size_t num_occupied_slots; +} async_image_task_fifo; + +static bool initialized = false; +static pthread_t images_thread; +static mgui_hashmap images; +static pthread_mutex_t mutex; +static sem_t task_sem; +static async_image_task_fifo tasks; +static uint32_t update_counter = 0; + +static size_t align_up(size_t value, size_t alignment) { + size_t over = value % alignment; + if(over == 0) + return value; + return value - over + alignment; +} + +static void async_image_task_fifo_init(async_image_task_fifo *self) { + self->tasks = NULL; + self->capacity = 0; + self->start_index = 0; + self->num_occupied_slots = 0; +} + +static void async_image_task_fifo_deinit(async_image_task_fifo *self) { + mgui_free(self->tasks); + self->tasks = NULL; + self->capacity = 0; + self->start_index = 0; + self->num_occupied_slots = 0; +} + +static void async_image_task_fifo_ensure_capacity(async_image_task_fifo *self, size_t new_capacity) { + if(self->capacity >= new_capacity) + return; + + size_t capacity = self->capacity; + if(capacity == 0) + capacity = 8; + + while(capacity < new_capacity) { + capacity = capacity + (capacity >> 1); /* capacity *= 1.5 */ + } + + capacity = align_up(capacity, sizeof(async_image_task)); + self->tasks = mgui_realloc(self->tasks, capacity); + + /* Straighten circular buffer. For example: convert [4][5][][][1][2][3][][] to [][][][][1][2][3][4][5] */ + if(self->capacity > 0) { + const size_t num_allocated_slots = self->capacity / sizeof(async_image_task); + const size_t end_index = (self->start_index + self->num_occupied_slots) % num_allocated_slots; + if(end_index < self->start_index) { + for(size_t i = 0; i <= end_index; ++i) { + self->tasks[self->num_occupied_slots + i] = self->tasks[end_index - i]; + } + } + } + self->capacity = capacity; +} + +static void async_image_task_fifo_append(async_image_task_fifo *self, async_image_task *task) { + async_image_task_fifo_ensure_capacity(self, (self->num_occupied_slots + 1) * sizeof(async_image_task)); + const size_t num_allocated_slots = self->capacity / sizeof(async_image_task); + const size_t insert_index = (self->start_index + self->num_occupied_slots) % num_allocated_slots; + self->tasks[insert_index] = *task; + ++self->num_occupied_slots; +} + +/* pop first item if not empty. returns false if empty */ +static bool async_image_task_fifo_pop(async_image_task_fifo *self, async_image_task *task_out) { + if(self->num_occupied_slots == 0) + return false; + + *task_out = self->tasks[self->start_index]; + const size_t num_allocated_slots = self->capacity / sizeof(async_image_task); + self->start_index = (self->start_index + 1) % num_allocated_slots; + --self->num_occupied_slots; + return true; +} + +/* Return false from |callback| to stop the iteration */ +static void async_image_task_fifo_for_each(async_image_task_fifo *self, bool (*callback)(async_image_task *task, void *userdata), void *userdata) { + if(self->capacity == 0) + return; + + const size_t num_allocated_slots = self->capacity / sizeof(async_image_task); + for(size_t i = 0; i < self->num_occupied_slots; ++i) { + const size_t index = (self->start_index + i) % num_allocated_slots; + if(!callback(&self->tasks[index], userdata)) + return; + } +} + +static void* images_thread_callback(void *arg) { + (void)arg; + async_image_task task; + while(initialized) { + sem_wait(&task_sem); + if(!initialized) + break; + + pthread_mutex_lock(&mutex); + const bool has_task = async_image_task_fifo_pop(&tasks, &task); + const bool empty_task = !has_task || !task.async_image; + if(!empty_task) + ++task.async_image->ref_count; + pthread_mutex_unlock(&mutex); + + if(empty_task) + continue; + + /* TODO: Create thumbnail (resize to target size) and load that instead */ + if(mgl_image_load_from_file(&task.async_image->image, task.async_image->filepath) == 0) + task.async_image->state = MGUI_ASYNC_IMAGE_LOADED; + else + task.async_image->state = MGUI_ASYNC_IMAGE_FAILED_TO_LOAD; + + pthread_mutex_lock(&mutex); + if(task.async_image->ref_count > 0) + --task.async_image->ref_count; + pthread_mutex_unlock(&mutex); + } + return NULL; +} + +void mgui_async_image_init() { + if(!initialized) { + initialized = true; + mgui_hashmap_init(&images); + async_image_task_fifo_init(&tasks); + + if(sem_init(&task_sem, 0, 0) != 0) { + fprintf(stderr, "mgui error: failed to initialize images semaphore\n"); + abort(); + } + + if(pthread_mutex_init(&mutex, NULL) != 0) { + fprintf(stderr, "mgui error: failed to initialize images mutex\n"); + abort(); + } + + if(pthread_create(&images_thread, NULL, images_thread_callback, NULL) != 0) { + fprintf(stderr, "mgui error: failed to initialize images thread\n"); + abort(); + } + } +} + +static bool images_free(void *value, void *userdata) { + (void)userdata; + mgui_async_image *async_image = value; + mgl_image_unload(&async_image->image); + mgl_texture_unload(&async_image->texture); + mgui_free(async_image->filepath); + mgui_free(async_image); + return true; +} + +void mgui_async_image_deinit() { + if(initialized) { + initialized = false; + sem_post(&task_sem); + pthread_join(images_thread, NULL); + pthread_mutex_destroy(&mutex); + sem_destroy(&task_sem); + + async_image_task task; + while(async_image_task_fifo_pop(&tasks, &task)) {} + + async_image_task_fifo_deinit(&tasks); + mgui_hashmap_for_each(&images, images_free, NULL); + mgui_hashmap_deinit(&images); + } +} + +static bool remove_unreferenced_tasks(async_image_task *task, void *userdata) { + (void)userdata; + if(task->async_image) + return true; + + if(task->async_image->updated != update_counter) { + task->async_image = NULL; + return false; + } else { + return true; + } +} + +static bool images_unload_unreferenced(void *value, void *userdata) { + (void)userdata; + mgui_async_image *async_image = value; + if(async_image->updated != update_counter) { + bool remove_image = false; + if(async_image->state != MGUI_ASYNC_IMAGE_LOADING) { + async_image->state = MGUI_ASYNC_IMAGE_UNLOADED; + mgl_image_unload(&async_image->image); + mgl_texture_unload(&async_image->texture); + + pthread_mutex_lock(&mutex); + /* TODO: Check if this actually happens */ + if(async_image->ref_count == 0) { + mgui_free(async_image->filepath); + mgui_free(async_image); + remove_image = true; + } + pthread_mutex_unlock(&mutex); + } + return remove_image; + } else { + return false; + } +} + +void mgui_async_image_unload_unreferenced() { + pthread_mutex_lock(&mutex); + async_image_task_fifo_for_each(&tasks, remove_unreferenced_tasks, NULL); + pthread_mutex_unlock(&mutex); + + mgui_hashmap_for_each_erase(&images, images_unload_unreferenced, NULL); + + ++update_counter; +} + +mgui_async_image* mgui_async_image_get_by_path(const char *filepath) { + const size_t filepath_len = strlen(filepath); + + pthread_mutex_lock(&mutex); + void *value; + if(!mgui_hashmap_get(&images, filepath, filepath_len, &value)) { + mgui_async_image *async_image = mgui_alloc(sizeof(mgui_async_image)); + async_image->image.data = NULL; + async_image->texture.id = 0; + async_image->hash = 0; + async_image->state = MGUI_ASYNC_IMAGE_LOADING; + async_image->updated = update_counter; + async_image->ref_count = 0; + async_image->filepath = mgui_alloc(filepath_len + 1); + memcpy(async_image->filepath, filepath, filepath_len); + async_image->filepath[filepath_len] = '\0'; + mgui_hashmap_insert(&images, filepath, filepath_len, async_image, &async_image->hash); + value = async_image; + + async_image_task task = { + .async_image = async_image + }; + async_image_task_fifo_append(&tasks, &task); + sem_post(&task_sem); + } + + mgui_async_image *async_image = value; + ++async_image->ref_count; + pthread_mutex_unlock(&mutex); + + return async_image; +} + +void mgui_async_image_update(mgui_async_image *self) { + if(self->state == MGUI_ASYNC_IMAGE_LOADED) { + self->state = MGUI_ASYNC_IMAGE_APPLIED; + if(mgl_texture_init(&self->texture) == 0) + mgl_texture_load_from_image(&self->texture, &self->image, NULL); + mgl_image_unload(&self->image); + } else if(self->state == MGUI_ASYNC_IMAGE_UNLOADED) { + self->state = MGUI_ASYNC_IMAGE_LOADING; + async_image_task task = { + .async_image = self + }; + async_image_task_fifo_append(&tasks, &task); + sem_post(&task_sem); + } + self->updated = update_counter; +} + +void mgui_async_image_unref(mgui_async_image *self) { + pthread_mutex_lock(&mutex); + if(self->ref_count > 0) + --self->ref_count; + pthread_mutex_unlock(&mutex); +} diff --git a/src/hashmap.c b/src/hashmap.c index 1625101..4c31e46 100644 --- a/src/hashmap.c +++ b/src/hashmap.c @@ -13,7 +13,7 @@ #endif #define HASH_TO_INDEX(hash) (hash & (CAP_NUM_ENTRIES(self->capacity)-1)) -#define HASHMAP_ENTRY_GET_KEY(entry) ((char*)entry + sizeof(mgui_hashmap_entry) + (entry->key_size)) +#define HASHMAP_ENTRY_GET_KEY(entry) ((char*)(entry) + sizeof(mgui_hashmap_entry)) /* |align| should be a multiple of 2 */ static size_t align_to(size_t value, size_t align) { @@ -37,7 +37,7 @@ void mgui_hashmap_deinit(mgui_hashmap *self) { mgui_hashmap_entry *entry = self->entries[i]; while(entry) { mgui_hashmap_entry *next = entry->next; - mgui_free(next); + mgui_free(entry); entry = next; } } @@ -129,8 +129,10 @@ bool mgui_hashmap_get(mgui_hashmap *self, const char *key, size_t key_size, void bool mgui_hashmap_get_by_hash(mgui_hashmap *self, const char *key, size_t key_size, uint64_t hash, void **value_out) { assert(hash == mgui_hashmap_hash(key, key_size)); - const size_t index = HASH_TO_INDEX(hash); + if(!self->entries) + return false; + const size_t index = HASH_TO_INDEX(hash); mgui_hashmap_entry *entry = self->entries[index]; while(entry) { if(hash == entry->hash && key_size == entry->key_size && memcmp(key, HASHMAP_ENTRY_GET_KEY(entry), key_size) == 0) { @@ -153,3 +155,42 @@ uint64_t mgui_hashmap_hash(const char *data, size_t size) { } return hash; } + +void mgui_hashmap_for_each(mgui_hashmap *self, bool(*callback)(void *value, void *userdata), void *userdata) { + if(!self->entries) + return; + + for(size_t i = 0; i < CAP_NUM_ENTRIES(self->capacity); ++i) { + mgui_hashmap_entry *entry = self->entries[i]; + while(entry) { + if(!callback(entry->value, userdata)) + return; + entry = entry->next; + } + } +} + +void mgui_hashmap_for_each_erase(mgui_hashmap *self, bool(*callback)(void *value, void *userdata), void *userdata) { + if(!self->entries) + return; + + for(size_t i = 0; i < CAP_NUM_ENTRIES(self->capacity); ++i) { + mgui_hashmap_entry *entry = self->entries[i]; + mgui_hashmap_entry *prev_entry = NULL; + while(entry) { + mgui_hashmap_entry *next = entry->next; + if(callback(entry->value, userdata)) { + /* Remove entry by replacing this entry with the next entry */ + if(prev_entry) + prev_entry->next = next; + else + self->entries[i] = next; + + mgui_free(entry); + } else { + prev_entry = entry; + } + entry = next; + } + } +} diff --git a/src/mgui/button.c b/src/mgui/button.c index 00db361..d77750d 100644 --- a/src/mgui/button.c +++ b/src/mgui/button.c @@ -12,6 +12,10 @@ static int min_int(int a, int b) { } mgui_button* mgui_button_create(const char *str, size_t size, unsigned char character_size) { + /* TODO: Make this work for character size >= 100 */ + if(character_size >= 100) + character_size = 99; + mgui_button *button = mgui_alloc(sizeof(mgui_button)); mgui_widget_init(&button->widget, MGUI_WIDGET_BUTTON); button->background.position = (mgl_vec2f){ 0.0f, 0.0f }; diff --git a/src/mgui/image.c b/src/mgui/image.c index 1a0871f..9f50b9c 100644 --- a/src/mgui/image.c +++ b/src/mgui/image.c @@ -2,12 +2,15 @@ #include "../../include/resource_loader.h" #include "../../include/common.h" #include "../../include/alloc.h" +#include "../../include/async_image.h" #include #include -#include +#include +#include #include -/* TODO: Load image asynchronously and support network files */ +/* TODO: Support network files */ +/* TODO: Set a target size and use that for calculating size and resize image to that */ static mgl_vec2i wrap_to_size_x(mgl_vec2i size, int clamp_size) { mgl_vec2i new_size; @@ -52,13 +55,24 @@ static mgl_vec2i clamp_to_size(mgl_vec2i size, mgl_vec2i clamp_size) { mgui_image* mgui_image_create(const char *filepath) { mgui_image *image = mgui_alloc(sizeof(mgui_image)); mgui_widget_init(&image->widget, MGUI_WIDGET_IMAGE); - (void)filepath; - /* TODO: Use |filepath| */ mgl_sprite_init(&image->sprite, NULL); + image->max_size = (mgl_vec2i){ 0, 0 }; + + if(filepath) { + image->async_image = mgui_async_image_get_by_path(filepath); + if(image->async_image->state == MGUI_ASYNC_IMAGE_APPLIED) + mgl_sprite_set_texture(&image->sprite, &image->async_image->texture); + } else { + image->async_image = NULL; + } + return image; } void mgui_image_destroy(mgui_image *image) { + if(image->async_image) + mgui_async_image_unref(image->async_image); + image->sprite.texture = NULL; mgui_free(image); } @@ -76,6 +90,7 @@ void mgui_image_set_position(mgui_image *self, mgl_vec2i position) { } void mgui_image_calculate_size(mgui_image *self, mgl_vec2i max_size) { + self->max_size = max_size; if(self->sprite.texture) { const mgl_vec2i texture_size = (mgl_vec2i){ self->sprite.texture->width, self->sprite.texture->height }; const mgl_vec2i new_size = clamp_to_size(texture_size, max_size); @@ -85,7 +100,7 @@ void mgui_image_calculate_size(mgui_image *self, mgl_vec2i max_size) { self->widget.size = new_size; } else { - self->widget.size = (mgl_vec2i){ 0, 0 }; + self->widget.size = (mgl_vec2i){ 1, 1 }; } } @@ -97,9 +112,14 @@ void mgui_image_on_event(mgui_image *self, mgl_window *window, mgl_event *event) } void mgui_image_draw(mgui_image *self, mgl_window *window) { - if(self->sprite.texture) { - const mgl_vec2i texture_size = (mgl_vec2i){ self->sprite.texture->width, self->sprite.texture->height }; - if(mgui_rectangle_intersects_with_scissor((mgl_vec2i){ self->sprite.position.x, self->sprite.position.y }, texture_size, window)) - mgl_sprite_draw(mgl_get_context(), &self->sprite); + if(self->async_image && mgui_rectangle_intersects_with_scissor((mgl_vec2i){ self->sprite.position.x, self->sprite.position.y }, self->widget.size, window)) { + mgui_async_image_update(self->async_image); + if(self->async_image->state == MGUI_ASYNC_IMAGE_APPLIED) { + mgl_sprite_set_texture(&self->sprite, &self->async_image->texture); + /* TODO: Check if this is correct when taking margin into consideration */ + mgui_image_calculate_size(self, self->max_size); + if(self->sprite.texture && self->sprite.texture->id) + mgl_sprite_draw(mgl_get_context(), &self->sprite); + } } } diff --git a/src/mgui/label.c b/src/mgui/label.c index e380aac..4f5b51f 100644 --- a/src/mgui/label.c +++ b/src/mgui/label.c @@ -14,6 +14,10 @@ static int min_int(int a, int b) { } mgui_label* mgui_label_create(const char *str, size_t size, unsigned char character_size) { + /* TODO: Make this work for character size >= 100 */ + if(character_size >= 100) + character_size = 99; + mgui_label *label = mgui_alloc(sizeof(mgui_label)); mgui_widget_init(&label->widget, MGUI_WIDGET_LABEL); label->str = mgui_alloc(size); diff --git a/src/mgui/list.c b/src/mgui/list.c index 687906d..ee58616 100644 --- a/src/mgui/list.c +++ b/src/mgui/list.c @@ -16,6 +16,7 @@ mgui_list* mgui_list_create(mgui_list_direction direction) { mgui_list *list = mgui_alloc(sizeof(mgui_list)); mgui_widget_init(&list->widget, MGUI_WIDGET_LIST); list->direction = direction; + list->spacing = 0; list->position = (mgl_vec2i){ 0, 0 }; list->items = NULL; list->items_capacity = 0; @@ -40,14 +41,25 @@ mgui_list* mgui_widget_to_list(mgui_widget *widget) { return (mgui_list*)widget; } +void mgui_list_set_spacing(mgui_list *self, int spacing) { + /* TODO: Multiply by scaling */ + self->spacing = spacing; + /* TODO: mgui_list_calculate_size */ +} + void mgui_list_set_position(mgui_list *self, mgl_vec2i position) { self->position = position; } void mgui_list_calculate_size(mgui_list *self, mgl_vec2i max_size) { + const int total_spacing = max_int(0, self->num_items - 1) * self->spacing; + mgl_vec2i size = (mgl_vec2i){ 0, 0 }; switch(self->direction) { case MGUI_LIST_HORIZONITAL: { + size.x = total_spacing; + max_size.x = max_int(0, max_size.x - total_spacing); + int num_expanded_widgets = 0; for(size_t i = 0; i < self->num_items; ++i) { mgui_widget *widget = self->items[i].widget; @@ -78,6 +90,9 @@ void mgui_list_calculate_size(mgui_list *self, mgl_vec2i max_size) { break; } case MGUI_LIST_VERTICAL: { + size.y = total_spacing; + max_size.y = max_int(0, max_size.y - total_spacing); + int num_expanded_widgets = 0; for(size_t i = 0; i < self->num_items; ++i) { mgui_widget *widget = self->items[i].widget; @@ -150,22 +165,31 @@ void mgui_list_draw(mgui_list *self, mgl_window *window) { /* TODO: Only do this when a direct child widget is dirty */ //mgui_list_calculate_size(self); + mgl_scissor scissor; + mgl_window_get_scissor(window, &scissor); + switch(self->direction) { case MGUI_LIST_HORIZONITAL: { for(size_t i = 0; i < self->num_items; ++i) { mgui_widget *widget = self->items[i].widget; - mgui_widget_set_position(widget, position); + mgui_widget_set_position(widget, (mgl_vec2i){ position.x + widget->margin.left, position.y + widget->margin.top }); mgui_widget_draw(widget, window); - position.x += widget->size.x; + position.x += widget->size.x + self->spacing; + + // if(position.x >= scissor.position.x + scissor.size.x) + // break; } break; } case MGUI_LIST_VERTICAL: { for(size_t i = 0; i < self->num_items; ++i) { mgui_widget *widget = self->items[i].widget; - mgui_widget_set_position(widget, position); + mgui_widget_set_position(widget, (mgl_vec2i){ position.x + widget->margin.left, position.y + widget->margin.top }); mgui_widget_draw(widget, window); - position.y += widget->size.y; + position.y += widget->size.y + self->spacing; + + // if(position.y >= scissor.position.y + scissor.size.y) + // break; } break; } diff --git a/src/mgui/mgui.c b/src/mgui/mgui.c index 3c1d955..0d9f20d 100644 --- a/src/mgui/mgui.c +++ b/src/mgui/mgui.c @@ -1,29 +1,47 @@ #include "../../include/mgui/mgui.h" #include "../../include/mgui/widget.h" +#include "../../include/async_image.h" #include #include +static mgl_vec2i root_widget_size; static mgl_clock global_timer; +static double frame_time; void mgui_init() { mgl_clock_init(&global_timer); + mgui_async_image_init(); + frame_time = 0.0; +} + +void mgui_deinit() { + mgui_async_image_deinit(); } void mgui_on_event(mgui_widget *root_widget, mgl_window *window, mgl_event *event) { if(event->type == MGL_EVENT_RESIZED) - mgui_widget_set_size(root_widget, (mgl_vec2i){ event->size.width, event->size.height }); + root_widget_size = (mgl_vec2i){ event->size.width, event->size.height }; mgui_widget_on_event(root_widget, window, event); } void mgui_draw(mgui_widget *root_widget, mgl_window *window) { /* TODO: Only do this if widget is dirty */ - mgui_widget_calculate_size(root_widget, root_widget->size); + mgui_widget_calculate_size(root_widget, root_widget_size); + mgui_widget_set_position(root_widget, (mgl_vec2i){ root_widget->margin.left, root_widget->margin.top }); mgui_widget_draw(root_widget, window); + mgui_async_image_unload_unreferenced(); + frame_time = mgl_clock_restart(&global_timer); + if(frame_time > 1.0) + frame_time = 1.0; } double mgui_get_seconds_since_last_update() { - double elapsed_time_sec = mgl_clock_restart(&global_timer); + double elapsed_time_sec = mgl_clock_get_elapsed_time_seconds(&global_timer); if(elapsed_time_sec > 1.0) elapsed_time_sec = 1.0; return elapsed_time_sec; } + +double mgui_get_frame_time_seconds() { + return frame_time; +} diff --git a/src/mgui/richtext.c b/src/mgui/richtext.c index 9b1acfc..77ee3c1 100644 --- a/src/mgui/richtext.c +++ b/src/mgui/richtext.c @@ -105,6 +105,10 @@ static void mgui_richtext_vertices_free(mgui_richtext *self, size_t vertex_index } mgui_richtext* mgui_richtext_create(const char *str, size_t size, unsigned char character_size) { + /* TODO: Make this work for character size >= 100 */ + if(character_size >= 100) + character_size = 99; + mgui_richtext *richtext = mgui_alloc(sizeof(mgui_richtext)); mgui_widget_init(&richtext->widget, MGUI_WIDGET_RICHTEXT); richtext->str = mgui_alloc(size); @@ -120,6 +124,7 @@ mgui_richtext* mgui_richtext_create(const char *str, size_t size, unsigned char richtext->vertex_data[i].vertex_count = 0; } richtext->dirty = true; + richtext->vertices_dirty = true; return richtext; } @@ -177,7 +182,7 @@ static void mgui_richtext_append_glyph(mgui_richtext *self, size_t vertex_index, mgui_richtext_vertices_append(self, vertex_index, &top_right_vertex); } -static void mgui_richtext_update(mgui_richtext *self) { +static void mgui_richtext_update(mgui_richtext *self, bool build_vertices) { for(size_t i = 0; i < NUM_VERTEX_DATA; ++i) { mgui_richtext_vertices_clear(self, i); } @@ -231,9 +236,11 @@ static void mgui_richtext_update(mgui_richtext *self) { if(position.x + glyph.size.x > self->width) { position.x = 0; position.y += self->character_size; + self->render_size.y += self->character_size; } - mgui_richtext_append_glyph(self, vertex_index, position, color, &glyph); + if(build_vertices) + mgui_richtext_append_glyph(self, vertex_index, position, color, &glyph); position.x += glyph.advance + mgl_font_get_kerning(font, prev_codepoint, codepoint); self->render_size.x = max_int(self->render_size.x, position.x); } @@ -249,13 +256,16 @@ void mgui_richtext_calculate_size(mgui_richtext *self, mgl_vec2i max_size) { /* TODO: Do not update if not visible on screen? */ if(max_size.x != self->width) { self->width = max_size.x; - self->dirty = true; + const bool is_multiple_lines = self->render_size.y > (int)self->character_size; + if(is_multiple_lines || self->width < self->render_size.x) + self->dirty = true; } /* TODO: Instead of updating richtext vertices, calculcate the richtext bounds only and update the vertices in the draw function if dirty */ if(self->dirty) { self->dirty = false; - mgui_richtext_update(self); + mgui_richtext_update(self, false); + self->vertices_dirty = true; self->widget.size.x = self->render_size.x; self->widget.size.y = min_int(self->render_size.y, max_size.y); } @@ -271,9 +281,9 @@ void mgui_richtext_on_event(mgui_richtext *self, mgl_window *window, mgl_event * void mgui_richtext_draw(mgui_richtext *self, mgl_window *window) { if(mgui_rectangle_intersects_with_scissor(self->position, self->render_size, window)) { /* This can happen when the item is first not visible in its scissor and then becomes visible */ - if(self->dirty) { - self->dirty = false; - mgui_richtext_update(self); + if(self->vertices_dirty) { + self->vertices_dirty = false; + mgui_richtext_update(self, true); } const mgui_font_type font_types[NUM_VERTEX_DATA] = { @@ -295,6 +305,6 @@ void mgui_richtext_draw(mgui_richtext *self, mgl_window *window) { for(size_t i = 0; i < NUM_VERTEX_DATA; ++i) { mgui_richtext_vertices_free(self, i); } - self->dirty = true; + self->vertices_dirty = true; } } diff --git a/src/mgui/scrollview.c b/src/mgui/scrollview.c index c7a4075..a11862f 100644 --- a/src/mgui/scrollview.c +++ b/src/mgui/scrollview.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -68,14 +69,15 @@ void mgui_scrollview_set_position(mgui_scrollview *self, mgl_vec2i position) { void mgui_scrollview_calculate_size(mgui_scrollview *self, mgl_vec2i max_size) { self->widget.size = max_size; - if(self->widget.size.x == WIDGET_NATURAL_SIZE) - self->widget.size.x = 500; - if(self->widget.size.y == WIDGET_NATURAL_SIZE) + if(self->widget.size.x >= WIDGET_NATURAL_SIZE/2 || self->widget.size.y >= WIDGET_NATURAL_SIZE/2) { + self->widget.size.x = 500; self->widget.size.y = 600; + } + /* TODO: this assumes child list uses vertical scroll. Make this work for horizontal scroll as well */ if(self->child) - mgui_widget_calculate_size(self->child, (mgl_vec2i){ WIDGET_NATURAL_SIZE, WIDGET_NATURAL_SIZE }); + mgui_widget_calculate_size(self->child, (mgl_vec2i){ max_size.x, WIDGET_NATURAL_SIZE }); } void mgui_scrollview_on_event(mgui_scrollview *self, mgl_window *window, mgl_event *event) { @@ -90,7 +92,7 @@ void mgui_scrollview_on_event(mgui_scrollview *self, mgl_window *window, mgl_eve /* TODO: Check if visible in scissor */ void mgui_scrollview_draw(mgui_scrollview *self, mgl_window *window) { - const double frame_time = mgui_get_seconds_since_last_update(); + const double frame_time = mgui_get_frame_time_seconds(); self->mouse_scroll.x *= max_float(0.0f, (1.0f - frame_time * SCROLL_DEACCEL)); self->mouse_scroll.y *= max_float(0.0f, (1.0f - frame_time * SCROLL_DEACCEL)); @@ -127,15 +129,41 @@ void mgui_scrollview_draw(mgui_scrollview *self, mgl_window *window) { mgl_scissor prev_scissor; mgl_window_get_scissor(window, &prev_scissor); + /* TODO: Fix all this margin crap, it should be invisible */ + const int margin_width = self->widget.margin.top + self->widget.margin.bottom; + const int margin_height = self->widget.margin.top + self->widget.margin.bottom; + mgl_scissor new_scissor = { .position = self->position, - .size = self->widget.size + .size = (mgl_vec2i){ self->widget.size.x - margin_width, self->widget.size.y - margin_height } }; mgl_window_set_scissor(window, &new_scissor); - mgui_widget_set_position(self->child, (mgl_vec2i){ self->position.x + self->scroll.x, self->position.y + self->scroll.y }); + mgui_widget_set_position(self->child, + (mgl_vec2i){ self->position.x + self->scroll.x + self->child->margin.left, self->position.y + self->scroll.y + self->child->margin.top }); mgui_widget_draw(self->child, window); mgl_window_set_scissor(window, &prev_scissor); + + { + /* TODO: Fix all this margin crap, it should be invisible */ + const int margin_height = self->widget.margin.top + self->widget.margin.bottom; + const int height = self->widget.size.y - margin_height; + const double scrollbar_height_ratio = child_size.y == 0 ? 0.0 : (double)height / (double)child_size.y; + const int scrollbar_height = scrollbar_height_ratio * height; + + const double scroll_ratio = child_size.y == 0 ? 0.0 : (double)-self->scroll.y / (double)child_size.y; + const int scrollbar_offset_y = height * scroll_ratio; + + const int scrollbar_width = 5; + const int right = self->widget.size.x - self->widget.margin.left - self->widget.margin.right; + mgl_rectangle rect = { + .position = { self->position.x + right - scrollbar_width, self->position.y + scrollbar_offset_y }, + .size = { scrollbar_width, scrollbar_height }, + .color = { 55, 60, 68, 255 } + }; + + mgl_rectangle_draw(mgl_get_context(), &rect); + } } } diff --git a/src/mgui/widget.c b/src/mgui/widget.c index ed50154..4e93b8a 100644 --- a/src/mgui/widget.c +++ b/src/mgui/widget.c @@ -9,7 +9,6 @@ #include #include -/* TODO: Use margin */ /* TODO: Use alignment */ /* TODO: Use visible flag */ @@ -34,6 +33,7 @@ void mgui_widget_init(mgui_widget *self, mgui_widget_type type) { self->alignment = MGUI_WIDGET_ALIGN_TOP_LEFT; mgui_widget_set_margin(self, (mgui_margin){ 0, 0, 0, 0 }); self->size = (mgl_vec2i){ 0, 0 }; + self->userdata = NULL; } void mgui_widget_destroy(mgui_widget *widget) { @@ -104,26 +104,33 @@ void mgui_widget_set_position(mgui_widget *self, mgl_vec2i position) { } void mgui_widget_calculate_size(mgui_widget *self, mgl_vec2i max_size) { + const int margin_width = self->margin.left + self->margin.right; + const int margin_height = self->margin.top + self->margin.bottom; + const mgl_vec2i max_size_result = (mgl_vec2i){ max_size.x - margin_width, max_size.y - margin_height }; + switch(self->type) { case MGUI_WIDGET_LIST: - mgui_list_calculate_size(mgui_widget_to_list(self), max_size); + mgui_list_calculate_size(mgui_widget_to_list(self), max_size_result); break; case MGUI_WIDGET_SCROLLVIEW: - mgui_scrollview_calculate_size(mgui_widget_to_scrollview(self), max_size); + mgui_scrollview_calculate_size(mgui_widget_to_scrollview(self), max_size_result); break; case MGUI_WIDGET_BUTTON: - mgui_button_calculate_size(mgui_widget_to_button(self), max_size); + mgui_button_calculate_size(mgui_widget_to_button(self), max_size_result); break; case MGUI_WIDGET_LABEL: - mgui_label_calculate_size(mgui_widget_to_label(self), max_size); + mgui_label_calculate_size(mgui_widget_to_label(self), max_size_result); break; case MGUI_WIDGET_RICHTEXT: - mgui_richtext_calculate_size(mgui_widget_to_richtext(self), max_size); + mgui_richtext_calculate_size(mgui_widget_to_richtext(self), max_size_result); break; case MGUI_WIDGET_IMAGE: - mgui_image_calculate_size(mgui_widget_to_image(self), max_size); + mgui_image_calculate_size(mgui_widget_to_image(self), max_size_result); break; } + + self->size.x += margin_width; + self->size.y += margin_height; } void mgui_widget_set_has_parent(mgui_widget *self) { diff --git a/tests/main.c b/tests/main.c index 6261bcd..00fcd1f 100644 --- a/tests/main.c +++ b/tests/main.c @@ -4,15 +4,17 @@ #include "../include/mgui/button.h" #include "../include/mgui/label.h" #include "../include/mgui/richtext.h" +#include "../include/mgui/image.h" #include #include #include #include #include -static mgui_list* create_list_item(const char *title, const char *description) { +static mgui_list* create_list_item(const char *image_filepath, const char *title, const char *description) { mgui_list *container = mgui_list_create(MGUI_LIST_HORIZONITAL); - mgui_list_append(container, mgui_button_to_widget(mgui_button_create("button", 6, 30))); + //mgui_list_set_spacing(container, 10); + mgui_list_append(container, mgui_image_to_widget(mgui_image_create(image_filepath))); mgui_list *list = mgui_list_create(MGUI_LIST_VERTICAL); mgui_list_append(container, mgui_list_to_widget(list)); @@ -33,21 +35,27 @@ int main() { mgui_list *root = mgui_list_create(MGUI_LIST_VERTICAL); mgui_widget *root_widget = mgui_list_to_widget(root); - mgui_list_append(root, mgui_list_to_widget(create_list_item("Title", "Description"))); + mgui_list_append(root, mgui_list_to_widget(create_list_item(NULL, "Title", "Description"))); mgui_scrollview *scrollview = mgui_scrollview_create(); mgui_widget_set_expand(mgui_scrollview_to_widget(scrollview), false, true); + //mgui_widget_set_margin(mgui_scrollview_to_widget(scrollview), (mgui_margin){ 20, 20, 20, 20 }); mgui_list_append(root, mgui_scrollview_to_widget(scrollview)); mgui_list *list = mgui_list_create(MGUI_LIST_VERTICAL); mgui_scrollview_set_child(scrollview, mgui_list_to_widget(list)); - for(int i = 0; i < 30; ++i) { + //mgui_list_set_spacing(list, 30); + for(int i = 0; i < 60; ++i) { char text[256]; snprintf(text, sizeof(text), "hello world 東京 %d", i); - mgui_list_append(list, mgui_list_to_widget(create_list_item("User", text))); + mgui_list_append(list, mgui_list_to_widget(create_list_item(NULL, "User", text))); } - mgui_list_append(root, mgui_list_to_widget(create_list_item("Bottom", "bottom"))); + char text[256]; + snprintf(text, sizeof(text), "hello world 東京 %d", 333); + mgui_list_append(list, mgui_list_to_widget(create_list_item("depends/mgl/tests/X11.png", "User", text))); + + mgui_list_append(root, mgui_list_to_widget(create_list_item(NULL, "Bottom", "bottom"))); mgl_event event; while(mgl_window_is_open(&window)) { @@ -55,13 +63,14 @@ int main() { mgui_on_event(root_widget, &window, &event); } - mgl_window_clear(&window, (mgl_color){ 0, 0, 0, 0 }); + mgl_window_clear(&window, (mgl_color){ 18, 21, 26, 255 }); mgui_draw(root_widget, &window); mgl_window_display(&window); } mgui_widget_destroy(root_widget); mgl_window_deinit(&window); + mgui_deinit(); mgl_deinit(); return 0; } -- cgit v1.2.3