aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2021-12-20 10:26:12 +0100
committerdec05eba <dec05eba@protonmail.com>2021-12-21 20:22:33 +0100
commit44e987c8521a99519350a42292bcfcd28451dcbd (patch)
tree699015a5dd459e96e0b19f4836f7dcffc1e347de
parent6bb40bf0c5cd8ee8fb87640fd04b2c595f84c1d3 (diff)
Async load images
-rw-r--r--TODO8
m---------depends/mgl0
-rw-r--r--include/async_image.h45
-rw-r--r--include/hashmap.h4
-rw-r--r--include/mgui/image.h5
-rw-r--r--include/mgui/list.h2
-rw-r--r--include/mgui/mgui.h4
-rw-r--r--include/mgui/richtext.h1
-rw-r--r--include/mgui/widget.h9
-rw-r--r--src/async_image.c297
-rw-r--r--src/hashmap.c47
-rw-r--r--src/mgui/button.c4
-rw-r--r--src/mgui/image.c38
-rw-r--r--src/mgui/label.c4
-rw-r--r--src/mgui/list.c32
-rw-r--r--src/mgui/mgui.c24
-rw-r--r--src/mgui/richtext.c26
-rw-r--r--src/mgui/scrollview.c42
-rw-r--r--src/mgui/widget.c21
-rw-r--r--tests/main.c23
20 files changed, 583 insertions, 53 deletions
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
-Subproject 4537803743e51ecb01925e9c5b4bef44aad8386
+Subproject 347a6fa2795a6856c1ad3d623673b62f879227c
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 <mgl/graphics/image.h>
+#include <mgl/graphics/texture.h>
+#include <stdint.h>
+
+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 <mgl/graphics/sprite.h>
+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 <pthread.h>
+#include <semaphore.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+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 <mgl/mgl.h>
#include <mgl/window/event.h>
-#include <mgl/graphics/texture.h>
+#include <stdio.h>
+#include <stdlib.h>
#include <assert.h>
-/* 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 <mgl/window/event.h>
#include <mgl/system/clock.h>
+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 <mgl/mgl.h>
#include <mgl/window/window.h>
#include <mgl/window/event.h>
+#include <mgl/graphics/rectangle.h>
#include <limits.h>
#include <assert.h>
@@ -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 <stdio.h>
#include <stdlib.h>
-/* 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 <mgl/mgl.h>
#include <mgl/window/window.h>
#include <mgl/window/event.h>
#include <string.h>
#include <stdio.h>
-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;
}