From 319accba370d3f0872501de6386833fea140fbac Mon Sep 17 00:00:00 2001 From: dec05eba Date: Wed, 8 Dec 2021 03:37:27 +0100 Subject: Add richtext, support multi language --- depends/mgl | 2 +- include/mgui/richtext.h | 38 +++++++ include/mgui/widget.h | 1 + src/mgui/richtext.c | 266 ++++++++++++++++++++++++++++++++++++++++++++++++ src/mgui/scrollview.c | 6 +- src/mgui/widget.c | 12 +++ src/resource_loader.c | 12 ++- tests/main.c | 7 +- 8 files changed, 334 insertions(+), 10 deletions(-) create mode 100644 include/mgui/richtext.h create mode 100644 src/mgui/richtext.c diff --git a/depends/mgl b/depends/mgl index a77da0a..e52886f 160000 --- a/depends/mgl +++ b/depends/mgl @@ -1 +1 @@ -Subproject commit a77da0acc88c7fb861043fd0dcb9cc6536e591f1 +Subproject commit e52886f8bea55fb3c9ff973b16ed812549cd1f22 diff --git a/include/mgui/richtext.h b/include/mgui/richtext.h new file mode 100644 index 0000000..52706f5 --- /dev/null +++ b/include/mgui/richtext.h @@ -0,0 +1,38 @@ +#ifndef MGUI_RICHTEXT_H +#define MGUI_RICHTEXT_H + +#include "widget.h" +#include +#include + +typedef struct mgl_vertex mgl_vertex; + +typedef struct { + mgl_vertex *vertices; + size_t vertices_capacity; + size_t vertex_count; +} mgui_richtext_vertex_data; + +typedef struct { + mgui_widget widget; + char *str; + size_t str_size; + unsigned int character_size; + mgl_vec2i position; + mgl_vec2i render_size; + int width; + mgui_richtext_vertex_data vertex_data[2]; + bool dirty; +} mgui_richtext; + +mgui_richtext* mgui_richtext_create(const char *str, size_t size, unsigned char character_size); +mgui_widget* mgui_richtext_to_widget(mgui_richtext *list); +mgui_richtext* mgui_widget_to_richtext(mgui_widget *widget); + +void mgui_richtext_set_position(mgui_richtext *self, mgl_vec2i position); +void mgui_richtext_set_width(mgui_richtext *self, int width); +void mgui_richtext_on_event(mgui_richtext *self, mgl_window *window, mgl_event *event); +/* Returns the size of the widget */ +mgl_vec2i mgui_richtext_draw(mgui_richtext *self, mgl_window *window); + +#endif /* MGUI_RICHTEXT_H */ diff --git a/include/mgui/widget.h b/include/mgui/widget.h index 2edc3b1..ee3a9f1 100644 --- a/include/mgui/widget.h +++ b/include/mgui/widget.h @@ -12,6 +12,7 @@ typedef enum { MGUI_WIDGET_SCROLLVIEW, MGUI_WIDGET_BUTTON, MGUI_WIDGET_LABEL, + MGUI_WIDGET_RICHTEXT, MGUI_WIDGET_IMAGE } mgui_widget_type; diff --git a/src/mgui/richtext.c b/src/mgui/richtext.c new file mode 100644 index 0000000..2f7c163 --- /dev/null +++ b/src/mgui/richtext.c @@ -0,0 +1,266 @@ +#include "../../include/mgui/richtext.h" +#include "../../include/resource_loader.h" +#include "../../include/common.h" +#include "../../include/alloc.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#define TAB_WIDTH 4 +#define LATIN_INDEX 0 +#define CJK_INDEX 1 +#define NUM_VERTEX_DATA 2 + +/* TODO: Scale richtext by scale setting */ +/* TODO: Split multiple lines into multiple vertex draw calls and do not render the vertices outside the scissor */ +/* TODO: Cleanup vertex when deleting mgui_richtext widget */ + +static int round_float(float value) { + return value + 0.5f; +} + +static int max_int(int a, int b) { + return a >= b ? a : b; +} + +/* TODO: Is there a more efficient way to do this? maybe japanese characters have a specific bit-pattern? */ +static bool is_japanese_codepoint(uint32_t codepoint) { + return (codepoint >= 0x2E80 && codepoint <= 0x2FD5) /* Kanji radicals */ + || (codepoint >= 0x3000 && codepoint <= 0x303F) /* Punctuation */ + || (codepoint >= 0x3041 && codepoint <= 0x3096) /* Hiragana */ + || (codepoint >= 0x30A0 && codepoint <= 0x30FF) /* Katakana */ + || (codepoint >= 0x31F0 && codepoint <= 0x31FF) /* Miscellaneous symbols and characters 1 */ + || (codepoint >= 0x3220 && codepoint <= 0x3243) /* Miscellaneous symbols and characters 2 */ + || (codepoint >= 0x3280 && codepoint <= 0x337F) /* Miscellaneous symbols and characters 3 */ + || (codepoint >= 0x3400 && codepoint <= 0x4DB5) /* Kanji 1 */ + || (codepoint >= 0x4E00 && codepoint <= 0x9FCB) /* Kanji 2 */ + || (codepoint >= 0xF900 && codepoint <= 0xFA6A) /* Kanji 3 */ + || (codepoint >= 0xFF01 && codepoint <= 0xFF5E) /* Alphanumeric and punctuation (full width) */ + || (codepoint >= 0xFF5F && codepoint <= 0xFF9F); /* Katakana and punctuation (half width) */ +} + +static bool is_korean_codepoint(uint32_t codepoint) { + return codepoint >= 0xAC00 && codepoint <= 0xD7A3; +} + +/* TODO: Is there a more efficient way to do this? maybe chinese characters have a specific bit-pattern? */ +static bool is_chinese_codepoint(uint32_t codepoint) { + return (codepoint >= 0x4E00 && codepoint <= 0x9FFF) /* CJK Unified Ideographs */ + || (codepoint >= 0x3400 && codepoint <= 0x4DBF) /* CJK Unified Ideographs Extension A */ + || (codepoint >= 0x20000 && codepoint <= 0x2A6DF) /* CJK Unified Ideographs Extension B */ + || (codepoint >= 0x2A700 && codepoint <= 0x2B73F) /* CJK Unified Ideographs Extension C */ + || (codepoint >= 0x2B740 && codepoint <= 0x2B81F) /* CJK Unified Ideographs Extension D */ + || (codepoint >= 0x2B820 && codepoint <= 0x2CEAF) /* CJK Unified Ideographs Extension E */ + || (codepoint >= 0xF900 && codepoint <= 0xFAFF) /* CJK Compatibility Ideographs */ + || (codepoint >= 0x2F800 && codepoint <= 0x2FA1F); /* CJK Compatibility Ideographs Supplement */ +} + +/* TODO: Merge chinese, japanese and korean codepoints into one function since they share ranges */ +static bool is_cjk_codepoint(uint32_t codepoint) { + return is_chinese_codepoint(codepoint) || is_japanese_codepoint(codepoint) || is_korean_codepoint(codepoint); +} + +static void mgui_richtext_vertices_ensure_vertex(mgui_richtext *self, size_t vertex_index, size_t new_capacity) { + mgui_richtext_vertex_data *vertex_data = &self->vertex_data[vertex_index]; + if(vertex_data->vertices_capacity >= new_capacity) + return; + + size_t capacity = vertex_data->vertices_capacity; + if(capacity == 0) + capacity = 8; + + while(capacity < new_capacity) { + capacity = capacity + (capacity >> 1); /* capacity *= 1.5 */ + } + + vertex_data->vertices = mgui_realloc(vertex_data->vertices, capacity); + vertex_data->vertices_capacity = capacity; +} + +static void mgui_richtext_vertices_append(mgui_richtext *self, size_t vertex_index, const mgl_vertex *vertex) { + mgui_richtext_vertex_data *vertex_data = &self->vertex_data[vertex_index]; + mgui_richtext_vertices_ensure_vertex(self, vertex_index, (vertex_data->vertex_count + 1) * sizeof(mgl_vertex)); + vertex_data->vertices[vertex_data->vertex_count] = *vertex; + ++vertex_data->vertex_count; +} + +static void mgui_richtext_vertices_clear(mgui_richtext *self, size_t vertex_index) { + self->vertex_data[vertex_index].vertex_count = 0; +} + +mgui_richtext* mgui_richtext_create(const char *str, size_t size, unsigned char character_size) { + mgui_richtext *richtext = mgui_alloc(sizeof(mgui_richtext)); + mgui_widget_init(&richtext->widget, MGUI_WIDGET_RICHTEXT); + richtext->str = mgui_alloc(size); + richtext->str_size = size; + richtext->character_size = character_size; + memcpy(richtext->str, str, size); + richtext->position = (mgl_vec2i){ 0, 0 }; + richtext->render_size = (mgl_vec2i){ 0, 0 }; + richtext->width = 0; + for(size_t i = 0; i < NUM_VERTEX_DATA; ++i) { + richtext->vertex_data[i].vertices = NULL; + richtext->vertex_data[i].vertices_capacity = 0; + richtext->vertex_data[i].vertex_count = 0; + } + richtext->dirty = true; + return richtext; +} + +mgui_widget* mgui_richtext_to_widget(mgui_richtext *list) { + return &list->widget; +} + +mgui_richtext* mgui_widget_to_richtext(mgui_widget *widget) { + assert(widget->type == MGUI_WIDGET_RICHTEXT); + return (mgui_richtext*)widget; +} + +void mgui_richtext_set_position(mgui_richtext *self, mgl_vec2i position) { + self->position = position; +} + +void mgui_richtext_set_width(mgui_richtext *self, int width) { + self->width = width; +} + +void mgui_richtext_on_event(mgui_richtext *self, mgl_window *window, mgl_event *event) { + /* TODO: Implement */ + (void)self; + (void)window; + (void)event; +} + +static void mgui_richtext_append_glyph(mgui_richtext *self, size_t vertex_index, mgl_vec2f position, mgl_color color, mgl_font_glyph *glyph) { + const mgl_vertex top_left_vertex = { + .position = (mgl_vec2f){ round_float(position.x + glyph->position.x), round_float(position.y + glyph->position.y) }, + .texcoords = (mgl_vec2f){ round_float(glyph->texture_position.x), round_float(glyph->texture_position.y) }, + .color = color + }; + + const mgl_vertex top_right_vertex = { + .position = (mgl_vec2f){ round_float(position.x + glyph->position.x + glyph->size.x), round_float(position.y + glyph->position.y) }, + .texcoords = (mgl_vec2f){ round_float(glyph->texture_position.x) + round_float(glyph->texture_size.x), round_float(glyph->texture_position.y) }, + .color = color + }; + + const mgl_vertex bottom_left_vertex = { + .position = (mgl_vec2f){ round_float(position.x + glyph->position.x), round_float(position.y + glyph->position.y + glyph->size.y) }, + .texcoords = (mgl_vec2f){ round_float(glyph->texture_position.x), round_float(glyph->texture_position.y + glyph->texture_size.y) }, + .color = color + }; + + const mgl_vertex bottom_right_vertex = { + .position = (mgl_vec2f){ round_float(position.x + glyph->position.x + glyph->size.x), round_float(position.y + glyph->position.y + glyph->size.y) }, + .texcoords = (mgl_vec2f){ round_float(glyph->texture_position.x + glyph->texture_size.x), round_float(glyph->texture_position.y + glyph->texture_size.y) }, + .color = color + }; + + mgui_richtext_vertices_append(self, vertex_index, &top_right_vertex); + mgui_richtext_vertices_append(self, vertex_index, &top_left_vertex); + mgui_richtext_vertices_append(self, vertex_index, &bottom_left_vertex); + mgui_richtext_vertices_append(self, vertex_index, &bottom_left_vertex); + mgui_richtext_vertices_append(self, vertex_index, &bottom_right_vertex); + mgui_richtext_vertices_append(self, vertex_index, &top_right_vertex); +} + +static void mgui_richtext_update(mgui_richtext *self) { + for(size_t i = 0; i < NUM_VERTEX_DATA; ++i) { + mgui_richtext_vertices_clear(self, i); + } + + mgl_font *font = NULL; + mgl_vec2f position = (mgl_vec2f){ 0.0f, 0.0f }; + mgl_font_glyph glyph; + uint32_t prev_codepoint = 0; + size_t codepoint_index = 0; + mgl_color color = (mgl_color){ 255, 255, 255, 255 }; + int vertex_index = -1; + self->render_size = (mgl_vec2i){ 0, self->character_size }; + + const mgui_font_type font_types[NUM_VERTEX_DATA] = { + MGUI_FONT_LATIN, + MGUI_FONT_CJK + }; + + for(size_t i = 0; i < self->str_size;) { + unsigned char *cp = (unsigned char*)&self->str[i]; + uint32_t codepoint; + size_t clen; + if(!mgl_utf8_decode(cp, self->str_size - i, &codepoint, &clen)) { + codepoint = *cp; + clen = 1; + } + + int new_vertex_index; + if(is_cjk_codepoint(codepoint)) { + new_vertex_index = CJK_INDEX; + } else { + new_vertex_index = LATIN_INDEX; + } + + if(new_vertex_index != vertex_index) { + vertex_index = new_vertex_index; + font = mgui_get_font(font_types[vertex_index], self->character_size); + } + + if(codepoint == '\t') { + if(mgl_font_get_glyph(font, ' ', &glyph) == 0) { + position.x += (glyph.advance * TAB_WIDTH); + self->render_size.x = max_int(self->render_size.x, position.x); + } + } else if(codepoint == '\n') { + position.x = 0; + position.y += self->character_size; + self->render_size.y += self->character_size; + } else { + if(mgl_font_get_glyph(font, codepoint, &glyph) == 0) { + if(position.x + glyph.size.x > self->width) { + //position.x = 0; + //position.y += self->character_size; + } + + 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); + } + } + + i += clen; + ++codepoint_index; + prev_codepoint = codepoint; + } +} + +mgl_vec2i mgui_richtext_draw(mgui_richtext *self, mgl_window *window) { + /* TODO: Do not update if not visible on screen? */ + if(self->dirty) { + self->dirty = false; + mgui_richtext_update(self); + } + + if(mgui_rectangle_intersects_with_scissor(self->position, self->render_size, window)) { + const mgui_font_type font_types[NUM_VERTEX_DATA] = { + MGUI_FONT_LATIN, + MGUI_FONT_CJK + }; + + for(size_t i = 0; i < NUM_VERTEX_DATA; ++i) { + if(self->vertex_data[i].vertex_count == 0) + continue; + + const mgl_font *font = mgui_get_font(font_types[i], self->character_size); + mgl_texture_use(&font->texture); + mgl_vertices_draw(mgl_get_context(), self->vertex_data[i].vertices, self->vertex_data[i].vertex_count, MGL_PRIMITIVE_TRIANGLES, (mgl_vec2f){ self->position.x, self->position.y }); + } + + mgl_texture_use(NULL); + } + + return self->render_size; +} diff --git a/src/mgui/scrollview.c b/src/mgui/scrollview.c index ae9ad97..e5b8bb8 100644 --- a/src/mgui/scrollview.c +++ b/src/mgui/scrollview.c @@ -35,6 +35,7 @@ mgui_scrollview* mgui_widget_to_scrollview(mgui_widget *widget) { void mgui_scrollview_set_child(mgui_scrollview *self, mgui_widget *child) { assert(child != mgui_scrollview_to_widget(self)); self->child = child; + mgui_scrollview_set_width(self, self->size.x); } void mgui_scrollview_set_position(mgui_scrollview *self, mgl_vec2i position) { @@ -43,12 +44,13 @@ void mgui_scrollview_set_position(mgui_scrollview *self, mgl_vec2i position) { void mgui_scrollview_set_size(mgui_scrollview *self, mgl_vec2i size) { self->size = size; + mgui_scrollview_set_width(self, self->size.x); } void mgui_scrollview_set_width(mgui_scrollview *self, int width) { /* TODO: Call for child so text can wordwrap? but only if wordwrap is enabled in this scrollview */ - (void)self; - (void)width; + if(self->child) + mgui_widget_set_width(self->child, width); } void mgui_scrollview_on_event(mgui_scrollview *self, mgl_window *window, mgl_event *event) { diff --git a/src/mgui/widget.c b/src/mgui/widget.c index e9c8425..8a49715 100644 --- a/src/mgui/widget.c +++ b/src/mgui/widget.c @@ -3,6 +3,7 @@ #include "../../include/mgui/scrollview.h" #include "../../include/mgui/button.h" #include "../../include/mgui/label.h" +#include "../../include/mgui/richtext.h" #include "../../include/mgui/image.h" /* TODO: Use margin */ @@ -33,6 +34,9 @@ void mgui_widget_set_position(mgui_widget *self, mgl_vec2i position) { case MGUI_WIDGET_LABEL: mgui_label_set_position(mgui_widget_to_label(self), position); break; + case MGUI_WIDGET_RICHTEXT: + mgui_richtext_set_position(mgui_widget_to_richtext(self), position); + break; case MGUI_WIDGET_IMAGE: mgui_image_set_position(mgui_widget_to_image(self), position); break; @@ -53,6 +57,9 @@ void mgui_widget_set_width(mgui_widget *self, int width) { case MGUI_WIDGET_LABEL: mgui_label_set_width(mgui_widget_to_label(self), width); break; + case MGUI_WIDGET_RICHTEXT: + mgui_richtext_set_width(mgui_widget_to_richtext(self), width); + break; case MGUI_WIDGET_IMAGE: mgui_image_set_width(mgui_widget_to_image(self), width); break; @@ -73,6 +80,9 @@ void mgui_widget_on_event(mgui_widget *self, mgl_window *window, mgl_event *even case MGUI_WIDGET_LABEL: mgui_label_on_event(mgui_widget_to_label(self), window, event); break; + case MGUI_WIDGET_RICHTEXT: + mgui_richtext_on_event(mgui_widget_to_richtext(self), window, event); + break; case MGUI_WIDGET_IMAGE: mgui_image_on_event(mgui_widget_to_image(self), window, event); break; @@ -89,6 +99,8 @@ mgl_vec2i mgui_widget_draw(mgui_widget *self, mgl_window *window) { return mgui_button_draw(mgui_widget_to_button(self), window); case MGUI_WIDGET_LABEL: return mgui_label_draw(mgui_widget_to_label(self), window); + case MGUI_WIDGET_RICHTEXT: + return mgui_richtext_draw(mgui_widget_to_richtext(self), window); case MGUI_WIDGET_IMAGE: return mgui_image_draw(mgui_widget_to_image(self), window); } diff --git a/src/resource_loader.c b/src/resource_loader.c index d589373..2eb0c01 100644 --- a/src/resource_loader.c +++ b/src/resource_loader.c @@ -22,24 +22,28 @@ mgl_font* mgui_get_font(mgui_font_type type, unsigned int character_size) { if(!font_file) { const char *font_paths[2]; size_t num_font_paths = 0; + const char *font_name = ""; switch(type) { case MGUI_FONT_LATIN: { font_paths[0] = "/usr/share/fonts/noto/NotoSans-Regular.ttf"; font_paths[1] = "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf"; num_font_paths = 2; + font_name = "NotoSans-Regular.ttf"; break; } case MGUI_FONT_LATIN_BOLD: { font_paths[0] = "/usr/share/fonts/noto/NotoSans-Bold.ttf"; font_paths[1] = "/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf"; num_font_paths = 2; + font_name = "NotoSans-Bold.ttf"; break; } case MGUI_FONT_CJK: { - font_paths[0] = "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttf"; - font_paths[1] = "/usr/share/fonts/truetype/noto-cjk/NotoSansCJK-Regular.ttf"; + font_paths[0] = "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc"; + font_paths[1] = "/usr/share/fonts/truetype/noto-cjk/NotoSansCJK-Regular.ttc"; num_font_paths = 2; + font_name = "NotoSansCJK-Regular.ttc"; break; } } @@ -60,13 +64,13 @@ mgl_font* mgui_get_font(mgui_font_type type, unsigned int character_size) { } if(!successfully_loaded_file) - fprintf(stderr, "mgui warning: mgui_get_font failed to load font %d\n", (int)type); + fprintf(stderr, "mgui warning: mgui_get_font failed to load font %s\n", font_name); font_file_cache[type] = new_font_file; font_file = new_font_file; } - if(!font_file) + if(!font_file->data) return NULL; mgl_font *font = font_cache[type][character_size]; diff --git a/tests/main.c b/tests/main.c index eb1c24f..e1aa6f1 100644 --- a/tests/main.c +++ b/tests/main.c @@ -3,6 +3,7 @@ #include "../include/mgui/scrollview.h" #include "../include/mgui/button.h" #include "../include/mgui/label.h" +#include "../include/mgui/richtext.h" #include #include #include @@ -16,8 +17,8 @@ static mgui_list* create_list_item(const char *title, const char *description) { mgui_list *list = mgui_list_create(MGUI_LIST_VERTICAL); mgui_list_append(container, mgui_list_to_widget(list)); - mgui_list_append(list, mgui_label_to_widget(mgui_label_create(title, strlen(title), 30))); - mgui_list_append(list, mgui_label_to_widget(mgui_label_create(description, strlen(description), 24))); + mgui_list_append(list, mgui_richtext_to_widget(mgui_richtext_create(title, strlen(title), 30))); + mgui_list_append(list, mgui_richtext_to_widget(mgui_richtext_create(description, strlen(description), 24))); return container; } @@ -36,7 +37,7 @@ int main() { mgui_scrollview_set_child(scrollview, mgui_list_to_widget(list)); for(int i = 0; i < 30; ++i) { char text[256]; - snprintf(text, sizeof(text), "hello world %d", i); + snprintf(text, sizeof(text), "hello world 東京 %d", i); mgui_list_append(list, mgui_list_to_widget(create_list_item("User", text))); } -- cgit v1.2.3