#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 #include #include #define TAB_WIDTH 4 #define LATIN_INDEX 0 #define CJK_INDEX 1 #define NUM_VERTEX_DATA 2 typedef enum { MGUI_RICHTEXT_SIDE_LEFT, MGUI_RICHTEXT_SIDE_RIGHT } mgui_richtext_selection_side; typedef struct { size_t start_vertex_data_index; size_t start_vertex_index; mgui_richtext_selection_side start_side; size_t current_vertex_data_index; size_t current_vertex_index; mgui_richtext_selection_side current_side; bool selecting_text; mgui_richtext *selecting_widget; } mgui_richtext_selection; static mgui_richtext_selection selection = {0}; /* TODO: Scale richtext by scale setting */ /* TODO: Split multiple lines into multiple vertex draw calls and do not render the vertices outside the scissor */ /* Maybe make the rows global and add the visible text into the rows */ /* TODO: Cleanup vertex when deleting mgui_richtext widget */ /* TODO: Use global vertex buffers instead of mgl_vertices_draw. The global vertex buffers should only exist for the visible text items */ static int round_float(float value) { return value + 0.5f; } static int max_int(int a, int b) { return a >= b ? a : b; } static int min_int(int a, int b) { return a <= b ? a : b; } static int abs_int(int value) { return value >= 0 ? value : -value; } /* 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; } static void mgui_richtext_vertices_free(mgui_richtext *self, size_t vertex_index) { mgui_free(self->vertex_data[vertex_index].vertices); self->vertex_data[vertex_index].vertices = NULL; self->vertex_data[vertex_index].vertices_capacity = 0; self->vertex_data[vertex_index].vertex_count = 0; } 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); 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; richtext->vertices_dirty = true; return richtext; } void mgui_richtext_destroy(mgui_richtext *richtext) { if(selection.selecting_widget == richtext) { selection.selecting_text = false; selection.selecting_widget = NULL; } for(size_t i = 0; i < NUM_VERTEX_DATA; ++i) { mgui_free(richtext->vertex_data[i].vertices); } mgui_free(richtext->str); mgui_free(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; } 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, bool build_vertices) { 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; self->render_size.y += self->character_size; } 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); } } i += clen; ++codepoint_index; prev_codepoint = codepoint; } } 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; 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, 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); } } static bool get_first_vertex_index(mgui_richtext *self, size_t *vertex_data_index, size_t *vertex_index) { for(size_t j = 0; j < NUM_VERTEX_DATA; ++j) { mgui_richtext_vertex_data *vertex_data = &self->vertex_data[j]; if(vertex_data->vertex_count > 0) { *vertex_data_index = j; *vertex_index = 0; return true; } } return false; } static bool get_last_vertex_index(mgui_richtext *self, size_t *vertex_data_index, size_t *vertex_index) { for(size_t j = 0; j < NUM_VERTEX_DATA; ++j) { mgui_richtext_vertex_data *vertex_data = &self->vertex_data[j]; if(vertex_data->vertex_count > 0) { *vertex_data_index = j; *vertex_index = vertex_data->vertex_count - 6; return true; } } return false; } /* TODO: For the start record the coordinate. Then for the current take the row closest to the cursor and then the column */ /* Clamp to text bounding box */ static bool mgui_richtext_get_closest_vertex_to_point(mgui_richtext *self, mgl_vec2f pos, size_t *vertex_data_index, size_t *vertex_index, mgui_richtext_selection_side *side) { if(pos.x < self->position.x) { if(!get_first_vertex_index(self, vertex_data_index, vertex_index)) return false; *side = MGUI_RICHTEXT_SIDE_LEFT; } // TODO: BLALBLA SIDES if(pos.x > self->position.x + self->render_size.x) { if(!get_last_vertex_index(self, vertex_data_index, vertex_index)) return false; *side = MGUI_RICHTEXT_SIDE_LEFT; } if(pos.x < self->position.x || pos.x > self->position.x + self->render_size.x) return false; if(pos.y < self->position.y || pos.y > self->position.y + self->render_size.y) return false; /* TODO: Binary search, and when moving cursor make binary search relative to the vertex that was closest to the cursor the last time */ /* TODO: Get closest vertex instead of the vertex which the point is inside (make sure bounds are properly checked, for example the first row might be less than render_size.x in width) */ for(size_t j = 0; j < NUM_VERTEX_DATA; ++j) { mgui_richtext_vertex_data *vertex_data = &self->vertex_data[j]; for(size_t i = 0; i < vertex_data->vertex_count; i += 6) { mgl_vertex *top_left_vertex = &vertex_data->vertices[i + 1]; mgl_vertex *bottom_right_vertex = &vertex_data->vertices[i + 4]; mgl_vec2f vertex_pos = (mgl_vec2f){ self->position.x + top_left_vertex->position.x, self->position.y + top_left_vertex->position.y }; mgl_vec2f vertex_size = (mgl_vec2f){ bottom_right_vertex->position.x - top_left_vertex->position.x, bottom_right_vertex->position.y - top_left_vertex->position.y }; if(mgui_rectangle_contains(vertex_pos, vertex_size, pos)) { *vertex_data_index = j; *vertex_index = i; *side = pos.x - vertex_pos.x < vertex_size.x * 0.5f ? MGUI_RICHTEXT_SIDE_LEFT : MGUI_RICHTEXT_SIDE_RIGHT; return true; } } } return false; } // TODO: Text selection in draw void mgui_richtext_on_event(mgui_richtext *self, mgl_window *window, mgl_event *event) { (void)window; /* TODO: Reset text selection if text content is modified or if the text is modified in any other way (text size changed or anything else that affects text position or size) */ if(event->type == MGL_EVENT_MOUSE_BUTTON_PRESSED && event->mouse_button.button == MGL_BUTTON_LEFT && !selection.selecting_text) { mgl_vec2f mouse_pos = (mgl_vec2f){ event->mouse_button.x, event->mouse_button.y }; if(!mgui_richtext_get_closest_vertex_to_point(self, mouse_pos, &selection.start_vertex_data_index, &selection.start_vertex_index, &selection.start_side)) { selection.selecting_text = false; selection.selecting_widget = NULL; return; } selection.selecting_text = true; selection.selecting_widget = self; selection.current_vertex_data_index = selection.start_vertex_data_index; selection.current_vertex_index = selection.start_vertex_index; selection.current_side = selection.start_side; } else if(event->type == MGL_EVENT_MOUSE_BUTTON_RELEASED && event->mouse_button.button == MGL_BUTTON_LEFT && selection.selecting_widget == self) { selection.selecting_text = false; } else if(event->type == MGL_EVENT_MOUSE_MOVED && selection.selecting_text && selection.selecting_widget == self) { /* TODO: */ } else if(event->type == MGL_EVENT_KEY_PRESSED && event->key.code == MGL_KEY_C && event->key.control && selection.selecting_widget == self) { /* TODO: Get selected vertices text and copy to clipboard */ /*mgl_window_set_clipboard(window, "hello world", 11);*/ } } /* Returns -1 if invalid index */ static int mgui_richtext_get_line_from_vertex(mgui_richtext *self, size_t vertex_data_index, size_t vertex_index) { if(self->character_size == 0) return -1; if(vertex_data_index > NUM_VERTEX_DATA) return -1; if(vertex_index > self->vertex_data[vertex_data_index].vertex_count) return -1; mgui_richtext_vertex_data *vertex_data = &self->vertex_data[vertex_data_index]; mgl_vertex *top_left_vertex = &vertex_data->vertices[vertex_index + 1]; return top_left_vertex->position.y / self->character_size; } static mgl_vec2f selection_get_position(mgui_richtext *self, size_t vertex_data_index, size_t vertex_index, mgui_richtext_selection_side side) { mgl_vertex *vertex = &self->vertex_data[vertex_data_index].vertices[vertex_index + (side == MGUI_RICHTEXT_SIDE_LEFT ? 1 : 0)]; return vertex->position; } static void mgui_richtext_draw_selection_background(mgui_richtext *self, mgl_window *window) { (void)window; const int start_row = mgui_richtext_get_line_from_vertex(self, selection.start_vertex_data_index, selection.start_vertex_index); const int current_row = mgui_richtext_get_line_from_vertex(self, selection.current_vertex_data_index, selection.current_vertex_index); if(start_row == -1 || current_row == -1) return; mgl_rectangle rect = { .position = { 0.0f, 0.0f }, .size = { 0.0f, self->character_size }, .color = { 60, 60, 180, 255 } }; if(start_row == current_row) { const mgl_vec2f start_pos = selection_get_position(self, selection.start_vertex_data_index, selection.start_vertex_index, selection.start_side); const mgl_vec2f current_pos = selection_get_position(self, selection.current_vertex_data_index, selection.current_vertex_index, selection.current_side); rect.position.x = self->position.x + min_int(start_pos.x, current_pos.x); rect.position.y = self->position.y + (start_row * self->character_size); rect.size.x = abs_int(current_pos.x - start_pos.x); mgl_rectangle_draw(mgl_get_context(), &rect); return; } const int y1 = min_int(start_row, current_row); const int y2 = max_int(start_row, current_row); /* TODO: Optimize rendering */ for(int i = y1; i < y2; ++i) { rect.position.x = self->position.x; rect.position.y = self->position.y + (i * self->character_size); rect.size.x = self->render_size.x; mgl_rectangle_draw(mgl_get_context(), &rect); } } 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->vertices_dirty) { self->vertices_dirty = false; mgui_richtext_update(self, true); } const mgui_font_type font_types[NUM_VERTEX_DATA] = { MGUI_FONT_LATIN, MGUI_FONT_CJK }; if(selection.selecting_widget == self) { /* TODO: Only if moved */ mgl_vec2f mouse_pos = (mgl_vec2f){ window->cursor_position.x, window->cursor_position.y }; if(selection.selecting_text) { mgui_richtext_get_closest_vertex_to_point(self, mouse_pos, &selection.current_vertex_data_index, &selection.current_vertex_index, &selection.current_side); } mgui_richtext_draw_selection_background(self, window); } 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); } else { for(size_t i = 0; i < NUM_VERTEX_DATA; ++i) { mgui_richtext_vertices_free(self, i); } self->vertices_dirty = true; } }