System info & network
This commit is contained in:
807
code/gui/gui.cpp
Normal file
807
code/gui/gui.cpp
Normal file
@@ -0,0 +1,807 @@
|
||||
#include "gui.h"
|
||||
#include "../render/2d.h"
|
||||
#include "../render/render.h"
|
||||
#include "../lib/math.h"
|
||||
#include "../lib/color.h"
|
||||
#include "text_draw.h"
|
||||
#include "../debug/logger.h"
|
||||
#include "stdio.h"
|
||||
#include "string.h"
|
||||
#include "../platform.h"
|
||||
#include "../lib/hashing.h"
|
||||
|
||||
Gui_State global_gui_state;
|
||||
|
||||
void gui_button_draw_inner_text(Gui_Context *ctx, Rect r, const char *text, v4 color, Rect *actual_drawn_rect = NULL);
|
||||
|
||||
bool gui_init()
|
||||
{
|
||||
gui_context_init(&global_gui_state.default_context);
|
||||
global_gui_state.selected_context = &global_gui_state.default_context;
|
||||
bool success = gui_text_draw_init();
|
||||
return success;
|
||||
}
|
||||
|
||||
void gui_deinit()
|
||||
{
|
||||
gui_text_draw_deinit();
|
||||
}
|
||||
|
||||
void gui_context_init(Gui_Context *ctx)
|
||||
{
|
||||
ctx->width = 100;
|
||||
ctx->height = 100;
|
||||
|
||||
ctx-> last_frame_time = 0;
|
||||
ctx->current_frame_time = 0;
|
||||
|
||||
ctx-> active = NULL;
|
||||
ctx-> hot = NULL;
|
||||
ctx->possibly_hot = NULL;
|
||||
ctx->active_start_time = 0;
|
||||
ctx-> hot_start_time = 0;
|
||||
ctx->active_status = 0;
|
||||
|
||||
ctx->text_cursor_position = 0;
|
||||
ctx->text_length = 0;
|
||||
|
||||
ctx->windows = NULL;
|
||||
ctx->window_count = 0;
|
||||
ctx->window_capacity = 0;
|
||||
ctx->current_window = NULL;
|
||||
|
||||
ctx->id_count = 0;
|
||||
ctx->id_capacity = 8;
|
||||
ctx->id_stack = (Gui_Id*)p_alloc(sizeof(Gui_Id) * ctx->id_capacity);
|
||||
|
||||
ctx->input.pointer_position = {0, 0};
|
||||
ctx->input.absolute_pointer_position = {0, 0};
|
||||
ctx->input.mouse_pressed = false;
|
||||
ctx->input.mouse_pressed_this_frame = false;
|
||||
ctx->input.mouse_released_this_frame = false;
|
||||
ctx->input.text_cursor_move = 0;
|
||||
ctx->input.text[0] = '\0';
|
||||
|
||||
ctx->input.absolute_pointer_position_last_frame = {0, 0};
|
||||
|
||||
|
||||
ctx->style.font_size = 12;
|
||||
ctx->style.animation_base_time = 0.100;
|
||||
|
||||
ctx->style.text_color = v4{1.0, 1.0, 1.0, 1.0};
|
||||
ctx->style.text_align = GUI_ALIGN_CENTER;
|
||||
|
||||
|
||||
ctx->style.button_color = v4{0.4f, 0.4f, 0.4f, 1.0f}*0.8f;
|
||||
ctx->style.button_color_hovered = v4{0.3f, 0.3f, 0.3f, 1.0f}*0.9f;
|
||||
ctx->style.button_color_pressed = v4{0.1f, 0.1f, 0.1f, 1.0f};
|
||||
ctx->style.button_text_color = v4{1.0f, 1.0f, 1.0f, 1.0f};
|
||||
ctx->style.button_text_color_hovered = v4{1.0f, 1.0f, 1.0f, 1.0f};
|
||||
ctx->style.button_text_color_pressed = v4{1.0f, 1.0f, 1.0f, 1.0f};
|
||||
ctx->style.button_radius = 3;
|
||||
|
||||
ctx->style.slider_fill_color = {0.0f, 0.3f, 0.9f, 1.0f};
|
||||
|
||||
ctx->style.window_background_color = {0.01,0.01,0.01, 0.98};
|
||||
ctx->style.window_border_color = {1.0,0.06,0.0,1.0};
|
||||
ctx->style.window_background_color_inactive = {0.05,0.05,0.05, 0.95};
|
||||
ctx->style.window_border_color_inactive = {0.3,0.3,0.3, 1.0};
|
||||
ctx->style.window_corner_radius = 5;
|
||||
ctx->style.window_titlebar_color = ctx->style.window_border_color;
|
||||
ctx->style.window_titlebar_color_inactive = {0.1,0.1,0.1,0.1};
|
||||
}
|
||||
|
||||
void gui_context_select(Gui_Context *ctx)
|
||||
{
|
||||
global_gui_state.selected_context = ctx;
|
||||
}
|
||||
|
||||
void gui_frame_begin(Gui_Context *ctx, f64 curr_time)
|
||||
{
|
||||
ctx-> last_frame_time = ctx->current_frame_time;
|
||||
ctx->current_frame_time = curr_time;
|
||||
}
|
||||
|
||||
void gui_frame_begin(f64 curr_time)
|
||||
{
|
||||
gui_frame_begin(&global_gui_state.default_context, curr_time);
|
||||
}
|
||||
|
||||
void gui_frame_end(Gui_Context *ctx)
|
||||
{
|
||||
// Render windows
|
||||
for(u32 i = 0; i < ctx->window_count; i++)
|
||||
{
|
||||
Gui_Window *w = &ctx->windows[i];
|
||||
if(w->still_open)
|
||||
{
|
||||
Rect r_uv = Rect{0,0,1,-1}; // y is flipped when rendering framebuffer's textures
|
||||
r_2d_immediate_rectangle(w->r, v4{1,1,1,1}, r_uv, &w->framebuffer.color_texture);
|
||||
w->still_open = false; // Will be set to true if still open
|
||||
}
|
||||
}
|
||||
// @Performance: cleanup unused windows
|
||||
|
||||
// Fix state
|
||||
if(ctx->hot != ctx->possibly_hot)
|
||||
{
|
||||
ctx->hot = ctx->possibly_hot;
|
||||
ctx->hot_start_time = ctx->current_frame_time;
|
||||
}
|
||||
|
||||
ctx->input.mouse_pressed_this_frame = false;
|
||||
ctx->input.mouse_released_this_frame = false;
|
||||
|
||||
ctx->input.absolute_pointer_position_last_frame = ctx->input.absolute_pointer_position;
|
||||
|
||||
ctx->input.text[0] = '\0';
|
||||
ctx->input.text_cursor_move = 0;
|
||||
|
||||
ctx->possibly_hot = NULL;
|
||||
}
|
||||
|
||||
void gui_frame_end()
|
||||
{
|
||||
gui_frame_end(&global_gui_state.default_context);
|
||||
}
|
||||
|
||||
void gui_handle_event(Gui_Context *ctx, Event *e)
|
||||
{
|
||||
switch(e->type)
|
||||
{
|
||||
case EVENT_MOUSE_MOVE: {
|
||||
if(!e->mouse_move.relative)
|
||||
{
|
||||
ctx->input.pointer_position = e->mouse_move.position;
|
||||
ctx->input.absolute_pointer_position = e->mouse_move.position;
|
||||
}
|
||||
} break;
|
||||
case EVENT_KEY: {
|
||||
switch(e->key.key_code)
|
||||
{
|
||||
case KEY_MOUSE_LEFT: {
|
||||
ctx->input.mouse_pressed = e->key.pressed;
|
||||
if(e->key.pressed)
|
||||
ctx->input.mouse_pressed_this_frame = true;
|
||||
else
|
||||
ctx->input.mouse_released_this_frame = true;
|
||||
} break;
|
||||
case KEY_ARROW_LEFT: {
|
||||
if(e->key.pressed)
|
||||
ctx->input.text_cursor_move--;
|
||||
} break;
|
||||
case KEY_ARROW_RIGHT: {
|
||||
if(e->key.pressed)
|
||||
ctx->input.text_cursor_move++;
|
||||
} break;
|
||||
default: {
|
||||
} break;
|
||||
}
|
||||
} break;
|
||||
case EVENT_RESIZE: {
|
||||
|
||||
} break;
|
||||
case EVENT_TEXT: {
|
||||
strcat(ctx->input.text, e->text.data);
|
||||
} break;
|
||||
default: {
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
void gui_handle_event(Event *e)
|
||||
{
|
||||
gui_handle_event(&global_gui_state.default_context, e);
|
||||
}
|
||||
|
||||
// ### Widgets ###
|
||||
// Text
|
||||
void gui_text(Gui_Context *ctx, Rect r, const char *text)
|
||||
{
|
||||
// @Feature: Clip text to Rect r
|
||||
gui_text_draw(r, text, ctx->style.font_size, ctx->style.text_color);
|
||||
}
|
||||
|
||||
void gui_text(Rect r, const char *text)
|
||||
{
|
||||
gui_text(&global_gui_state.default_context, r, text);
|
||||
}
|
||||
|
||||
void gui_text_aligned(Gui_Context *ctx, Rect r, const char *text, Gui_Text_Align alignment)
|
||||
{
|
||||
// @Cleanup: this should not depend on setting state. We should have a function that gets alignment as an argument
|
||||
Gui_Text_Align old_alignment = ctx->style.text_align;
|
||||
ctx->style.text_align = alignment;
|
||||
gui_button_draw_inner_text(ctx, r, text, ctx->style.text_color);
|
||||
ctx->style.text_align = old_alignment;
|
||||
}
|
||||
|
||||
void gui_text_aligned(Rect r, const char *text, Gui_Text_Align alignment)
|
||||
{
|
||||
gui_text_aligned(&global_gui_state.default_context, r, text, alignment);
|
||||
}
|
||||
|
||||
|
||||
v2 gui_text_compute_size(Gui_Context *ctx, const char *text)
|
||||
{
|
||||
return gui_text_draw_size(text, ctx->style.font_size);
|
||||
}
|
||||
|
||||
v2 gui_text_compute_size(const char *text)
|
||||
{
|
||||
return gui_text_compute_size(&global_gui_state.default_context, text);
|
||||
}
|
||||
|
||||
// Button
|
||||
bool gui_button(Gui_Context *ctx, Rect r, const char *text)
|
||||
{
|
||||
Gui_Id widget_id = gui_id_from_pointer(ctx, text);
|
||||
bool behaviuor = gui_button_behaviuor(ctx, widget_id, r);
|
||||
|
||||
// Compute colors
|
||||
v4 button_color = ctx->style.button_color;
|
||||
v4 text_color = ctx->style.button_text_color;
|
||||
{
|
||||
if(ctx->hot == widget_id)
|
||||
{
|
||||
f64 delta_t = (ctx->current_frame_time - ctx->hot_start_time);
|
||||
f32 interpolation = clamp(0, 1, delta_t / ctx->style.animation_base_time);
|
||||
button_color = lerp(ctx->style.button_color, ctx->style.button_color_hovered, interpolation);
|
||||
text_color = lerp(ctx->style.button_text_color, ctx->style.button_text_color_hovered, interpolation);
|
||||
}
|
||||
if(ctx->active == widget_id)
|
||||
{
|
||||
f64 delta_t = (ctx->current_frame_time - ctx->active_start_time);
|
||||
f32 interpolation = clamp(0, 1, delta_t / ctx->style.animation_base_time);
|
||||
button_color = lerp(ctx->style.button_color_hovered, ctx->style.button_color_pressed, interpolation * 0.4 + 0.6);
|
||||
text_color = lerp(ctx->style.button_text_color_hovered, ctx->style.button_text_color_pressed, interpolation * 0.4 + 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw button and text
|
||||
r_2d_immediate_rounded_rectangle(r, ctx->style.button_radius, button_color);
|
||||
gui_button_draw_inner_text(ctx, r, text, text_color);
|
||||
|
||||
return behaviuor;
|
||||
}
|
||||
|
||||
bool gui_button(Rect r, const char *text)
|
||||
{
|
||||
return gui_button(&global_gui_state.default_context, r, text);
|
||||
}
|
||||
|
||||
void gui_button_draw_inner_text(Gui_Context *ctx, Rect r, const char *text, v4 color, Rect *actual_drawn_rect)
|
||||
{
|
||||
v2 text_size = gui_text_draw_size(text, ctx->style.font_size);
|
||||
// Alignment (center, left, right)
|
||||
v2 text_position = r.position + (r.size - text_size) * v2{0.5, 0.5};
|
||||
if(ctx->style.text_align == GUI_ALIGN_LEFT)
|
||||
text_position = r.position + (r.size - text_size) * v2{0, 0.5};
|
||||
if(ctx->style.text_align == GUI_ALIGN_RIGHT)
|
||||
text_position = r.position + (r.size - text_size) * v2{1, 0.5};
|
||||
// Draw
|
||||
Rect text_rect = { .position = text_position, .size = text_size };
|
||||
// @Feature: Clip text to Rect r
|
||||
gui_text_draw(text_rect, text, ctx->style.font_size, color);
|
||||
|
||||
if(actual_drawn_rect)
|
||||
*actual_drawn_rect = text_rect;
|
||||
}
|
||||
|
||||
// Slider
|
||||
bool gui_slider(Gui_Context *ctx, Rect r, f32 min, f32 max, f32 *value)
|
||||
{
|
||||
Gui_Id widget_id = gui_id_from_pointer(ctx, value);
|
||||
bool behaviour = gui_button_behaviuor(ctx, widget_id, r);
|
||||
|
||||
if(ctx->active == widget_id)
|
||||
{
|
||||
f32 pointer_ratio = (ctx->input.pointer_position.x - r.position.x) / r.size.x;
|
||||
*value = clamp(0.0f, 1.0f, pointer_ratio) * (max - min) + min;
|
||||
}
|
||||
|
||||
// Colors
|
||||
v4 button_color = ctx->style.button_color;
|
||||
v4 text_color = ctx->style.button_text_color;
|
||||
{
|
||||
f64 delta_t = (ctx->current_frame_time - ctx->hot_start_time);
|
||||
f32 interpolation = sin(10 * delta_t) * 0.5 + 0.5;
|
||||
if(ctx->hot == widget_id)
|
||||
{
|
||||
button_color = lerp(ctx->style.button_color, ctx->style.button_color_hovered, interpolation);
|
||||
text_color = lerp(ctx->style.button_text_color, ctx->style.button_text_color_hovered, interpolation);
|
||||
}
|
||||
if(ctx->active == widget_id)
|
||||
{
|
||||
button_color = lerp(ctx->style.button_color_hovered, ctx->style.button_color_pressed, interpolation * 0.4 + 0.6);
|
||||
text_color = lerp(ctx->style.button_text_color_hovered, ctx->style.button_text_color_pressed, interpolation * 0.4 + 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw
|
||||
f32 border = 2;
|
||||
f32 radius = ctx->style.button_radius;
|
||||
|
||||
// Draw background
|
||||
v4 background_color = ctx->style.button_color;
|
||||
r_2d_immediate_rounded_rectangle(r, radius, background_color); // Background
|
||||
|
||||
// Draw fill
|
||||
f32 ratio = (*value - min) / (max - min);
|
||||
Rect fill_r = r;
|
||||
fill_r.position += v2{border, border};
|
||||
fill_r.size = v2{maximum(0, fill_r.size.x - 2*border), maximum(0, fill_r.size.y - 2*border)};
|
||||
f32 fill_radius = maximum(0, radius - border);
|
||||
fill_r.size.x = fill_r.size.x * ratio;
|
||||
r_2d_immediate_rounded_rectangle(fill_r, fill_radius, ctx->style.slider_fill_color);
|
||||
|
||||
// Draw border
|
||||
v4 border_color = ctx->style.button_color_pressed;
|
||||
Rect border_r = r;
|
||||
border_r.position += v2{border, border} * 0.5;
|
||||
border_r.size = v2{maximum(0, border_r.size.x - border), maximum(0, border_r.size.y - border)};
|
||||
f32 border_radius = maximum(0, radius - border*0.5);
|
||||
r_2d_immediate_rounded_rectangle_outline(border_r, border_radius, border_color, border);
|
||||
|
||||
// Draw value text
|
||||
char text[64];
|
||||
sprintf(text, "%f", *value);
|
||||
gui_button_draw_inner_text(ctx, r, text, text_color);
|
||||
|
||||
return behaviour || ctx->active == widget_id;
|
||||
}
|
||||
|
||||
bool gui_slider(Rect r, f32 min, f32 max, f32 *value)
|
||||
{
|
||||
return gui_slider(&global_gui_state.default_context, r, min, max, value);
|
||||
}
|
||||
|
||||
|
||||
// Images
|
||||
bool gui_image(Gui_Context *ctx, Rect r, r_texture *texture)
|
||||
{
|
||||
Gui_Id widget_id = gui_id_from_pointer(ctx, texture);
|
||||
|
||||
v4 color = {1,1,1,1};
|
||||
r_2d_immediate_rectangle(r, color, {0,0,1,1}, texture);
|
||||
|
||||
return gui_button_behaviuor(ctx, widget_id, r);;
|
||||
}
|
||||
|
||||
bool gui_image(Rect r, r_texture *texture)
|
||||
{
|
||||
return gui_image(&global_gui_state.default_context, r, texture);
|
||||
}
|
||||
|
||||
bool gui_image(Gui_Context *ctx, Rect r, const u8 *bmp, u32 width, u32 height, u32 channels, u32 flags)
|
||||
{
|
||||
r_texture texture = r_texture_create((u8*)bmp, {width, height}, flags | R_TEXTURE_DONT_OWN);
|
||||
bool result = gui_image(ctx, r, &texture);
|
||||
r_texture_destroy(&texture);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool gui_image(Rect r, const u8 *bmp, u32 width, u32 height, u32 channels, u32 flags)
|
||||
{
|
||||
return gui_image(&global_gui_state.default_context, r, bmp, width, height, channels, flags);
|
||||
}
|
||||
|
||||
|
||||
// Text input
|
||||
bool gui_text_input(Gui_Context *ctx, Rect r, char *text, u64 max_size)
|
||||
{
|
||||
Gui_Id widget_id = gui_id_from_pointer(ctx, text);
|
||||
bool behaviour = gui_text_input_behaviuor(ctx, widget_id, r);
|
||||
bool edited = false;
|
||||
|
||||
// Cursor, mouse click, input from keyboard/os
|
||||
if(ctx->active == widget_id && ctx->input.mouse_pressed_this_frame)
|
||||
{
|
||||
ctx->text_length = strlen(text);
|
||||
ctx->text_cursor_position = ctx->text_length;
|
||||
}
|
||||
|
||||
// Move cursors between UTF8 codepoints (not bytes)
|
||||
if(ctx->input.text_cursor_move != 0)
|
||||
{
|
||||
while(ctx->input.text_cursor_move > 0)
|
||||
{
|
||||
if(text[ctx->text_cursor_position] == '\0')
|
||||
{
|
||||
ctx->input.text_cursor_move = 0;
|
||||
break;
|
||||
}
|
||||
ctx->text_cursor_position += utf8_bytes_to_next_valid_codepoint(text, ctx->text_cursor_position);
|
||||
ctx->input.text_cursor_move--;
|
||||
}
|
||||
while(ctx->input.text_cursor_move < 0)
|
||||
{
|
||||
if(ctx->text_cursor_position == 0)
|
||||
{
|
||||
ctx->input.text_cursor_move = 0;
|
||||
break;
|
||||
}
|
||||
ctx->text_cursor_position -= utf8_bytes_to_prev_valid_codepoint(text, ctx->text_cursor_position);
|
||||
ctx->input.text_cursor_move++;
|
||||
}
|
||||
}
|
||||
|
||||
if(ctx->active == widget_id && ctx->input.text[0] != 0)
|
||||
{
|
||||
// @Bug: Should iterate on utf8 codepoints. If we don't, there's the possibility
|
||||
// of inserting half of a multi-byte codepoint.
|
||||
for(char *c = ctx->input.text; *c != 0; c++)
|
||||
{
|
||||
if(*c == 0x08) // Backspace
|
||||
{
|
||||
if(ctx->text_cursor_position > 0)
|
||||
{
|
||||
u32 codepoint_bytes = utf8_bytes_to_prev_valid_codepoint(text, ctx->text_cursor_position);
|
||||
u64 from_index = ctx->text_cursor_position;
|
||||
u64 to_index = ctx->text_cursor_position - codepoint_bytes;
|
||||
memmove(text + to_index, text + from_index, ctx->text_length + 1 - from_index);
|
||||
ctx->text_length -= codepoint_bytes;
|
||||
ctx->text_cursor_position -= codepoint_bytes;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if(*c == 0x7F) // Delete
|
||||
{
|
||||
if(ctx->text_cursor_position < ctx->text_length)
|
||||
{
|
||||
u32 codepoint_bytes = utf8_bytes_to_next_valid_codepoint(text, ctx->text_cursor_position);
|
||||
u64 from_index = ctx->text_cursor_position + codepoint_bytes;
|
||||
u64 to_index = ctx->text_cursor_position;
|
||||
memmove(text + to_index, text + from_index, ctx->text_length + 1 - from_index);
|
||||
ctx->text_length -= codepoint_bytes;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if(ctx->text_length < max_size - 1) // Leave space for 0 terminator
|
||||
{
|
||||
memmove(text + ctx->text_cursor_position + 1, text + ctx->text_cursor_position, ctx->text_length + 1 - ctx->text_cursor_position);
|
||||
text[ctx->text_cursor_position] = *c;
|
||||
ctx->text_length += 1;
|
||||
ctx->text_cursor_position += 1;
|
||||
}
|
||||
}
|
||||
|
||||
edited = true;
|
||||
}
|
||||
|
||||
r_2d_immediate_rounded_rectangle(r, ctx->style.button_radius, ctx->style.button_color);
|
||||
Rect text_rect;
|
||||
gui_button_draw_inner_text(ctx, r, text, ctx->style.button_text_color, &text_rect);
|
||||
|
||||
if(ctx->active == widget_id)
|
||||
{
|
||||
// Draw cursor
|
||||
f64 delta_t = ctx->current_frame_time - ctx->active_start_time;
|
||||
f32 u = clamp(0, 1, sin(delta_t * 5) * 0.7 + 0.6);
|
||||
v4 cursor_color = ctx->style.button_text_color;
|
||||
cursor_color *= lerp(0, cursor_color.a, u);
|
||||
|
||||
char replaced = text[ctx->text_cursor_position];
|
||||
text[ctx->text_cursor_position] = 0;
|
||||
v2 cursor_position;
|
||||
v2 text_size = gui_text_draw_size(text, ctx->style.font_size, &cursor_position);
|
||||
text[ctx->text_cursor_position] = replaced;
|
||||
|
||||
Rect cursor_rect =
|
||||
{
|
||||
.position = text_rect.position + cursor_position - v2{0, ctx->style.font_size},
|
||||
.size = ctx->style.font_size * v2{0.1, 0.9}
|
||||
};
|
||||
r_2d_immediate_rectangle(cursor_rect, cursor_color);
|
||||
}
|
||||
|
||||
return edited;
|
||||
}
|
||||
|
||||
bool gui_text_input(Rect r, char *text, u64 max_size)
|
||||
{
|
||||
return gui_text_input(&global_gui_state.default_context, r, text, max_size);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Windows
|
||||
bool gui_window_start(Gui_Context *ctx, Rect r, Gui_Id id)
|
||||
{
|
||||
gui_id_stack_push(ctx, id);
|
||||
Gui_Window *window = gui_window_by_id(ctx, r, id);
|
||||
window->still_open = true;
|
||||
gui_window_update_rect(ctx, window, r);
|
||||
u32 window_index = window - ctx->windows;
|
||||
|
||||
bool hovered = gui_is_hovered(ctx, id, r);
|
||||
if(hovered && ctx->input.mouse_pressed_this_frame)
|
||||
{
|
||||
// Bring window on top
|
||||
u32 move_count = ctx->window_count - 1 - window_index;
|
||||
if(move_count > 0)
|
||||
{
|
||||
Gui_Window tmp = *window;
|
||||
memmove(ctx->windows + window_index, ctx->windows + window_index + 1, sizeof(Gui_Window) * move_count);
|
||||
|
||||
ctx->windows[ctx->window_count - 1] = tmp;
|
||||
window_index = ctx->window_count - 1;
|
||||
window = &ctx->windows[window_index];
|
||||
}
|
||||
}
|
||||
|
||||
ctx->current_window = window;
|
||||
ctx->input.pointer_position = ctx->input.absolute_pointer_position - window->r.position;
|
||||
|
||||
ctx->old_framebuffer = r_render_state.current_framebuffer;
|
||||
r_framebuffer_select(&window->framebuffer);
|
||||
|
||||
|
||||
bool is_inactive = window_index != ctx->window_count-1;
|
||||
v4 background_color = is_inactive ? ctx->style.window_background_color_inactive :
|
||||
ctx->style.window_background_color;
|
||||
v4 border_color = is_inactive ? ctx->style.window_border_color_inactive :
|
||||
ctx->style.window_border_color;
|
||||
r_clear({0,0,0,0});
|
||||
Rect background_rect = {0.5, 0.5, floor(r.w)-1.0, floor(r.h)-1.0};
|
||||
r_2d_immediate_rounded_rectangle(background_rect, ctx->style.window_corner_radius, background_color);
|
||||
r_2d_immediate_rounded_rectangle_outline(background_rect, ctx->style.window_corner_radius, border_color, 1.0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool gui_window_start(Rect r, Gui_Id id)
|
||||
{
|
||||
return gui_window_start(&global_gui_state.default_context, r, id);
|
||||
}
|
||||
|
||||
void gui_window_end(Gui_Context *ctx)
|
||||
{
|
||||
gui_id_stack_pop(ctx);
|
||||
ctx->current_window = NULL;
|
||||
ctx->input.pointer_position = ctx->input.absolute_pointer_position;
|
||||
|
||||
r_framebuffer_select(ctx->old_framebuffer);
|
||||
}
|
||||
|
||||
void gui_window_end()
|
||||
{
|
||||
return gui_window_end(&global_gui_state.default_context);
|
||||
}
|
||||
|
||||
|
||||
bool gui_window_titlebar(Gui_Context *ctx, Rect r, const char *title, bool *close, v2 *move)
|
||||
{
|
||||
Gui_Id widget_id = gui_id_from_pointer(ctx, title);
|
||||
bool behaviour = gui_button_behaviuor(ctx, widget_id, r);
|
||||
if(close)
|
||||
*close = false;
|
||||
if(move)
|
||||
*move = {0,0};
|
||||
|
||||
// Background
|
||||
v4 titlebar_color = ctx->style.window_titlebar_color_inactive;
|
||||
if(ctx->current_window && ctx->current_window - ctx->windows == ctx->window_count - 1)
|
||||
{
|
||||
titlebar_color = ctx->style.window_titlebar_color;
|
||||
}
|
||||
r_2d_immediate_rounded_rectangle(r, ctx->style.window_corner_radius, titlebar_color);
|
||||
|
||||
// Title
|
||||
v2 title_size = gui_text_compute_size(title);
|
||||
Rect title_r = r;
|
||||
title_r.size = title_size;
|
||||
title_r.position = r.position + (r.size - title_r.size) / 2;
|
||||
gui_text(title_r, title);
|
||||
|
||||
// Exit button
|
||||
f32 smallest_side = minimum(r.w, r.h);
|
||||
f32 exit_size = smallest_side;
|
||||
Gui_Style exit_style = ctx->style;
|
||||
exit_style.button_color = v4{0.8f, 0.8f, 0.8f, 1.0f}*0.0f;
|
||||
exit_style.button_color_hovered = v4{1.0f, 0.0f, 0.0f, 1.0f}*1.0f;
|
||||
exit_style.button_color_pressed = v4{0.8f, 0.0f, 0.0f, 1.0f};
|
||||
exit_style.button_text_color = v4{1.0f, 1.0f, 1.0f, 1.0f};
|
||||
exit_style.button_text_color_hovered = v4{1.0f, 1.0f, 1.0f, 1.0f};
|
||||
exit_style.button_text_color_pressed = v4{1.0f, 1.0f, 1.0f, 1.0f};
|
||||
exit_style.button_radius = ctx->style.window_corner_radius;
|
||||
Gui_Style old_style = ctx->style;
|
||||
ctx->style = exit_style;
|
||||
Rect exit_button_r = {r.x + r.w - exit_size, r.y, exit_size, exit_size};
|
||||
if(gui_button(ctx, exit_button_r, "⨯"))
|
||||
{
|
||||
if(close)
|
||||
*close = true;
|
||||
behaviour = true;
|
||||
}
|
||||
ctx->style = old_style;
|
||||
|
||||
// Move
|
||||
if(ctx->active == widget_id && !ctx->input.mouse_pressed_this_frame)
|
||||
{
|
||||
if(move)
|
||||
{
|
||||
*move = ctx->input.absolute_pointer_position - ctx->input.absolute_pointer_position_last_frame;
|
||||
}
|
||||
}
|
||||
|
||||
return behaviour || ctx->active == widget_id;
|
||||
}
|
||||
|
||||
bool gui_window_titlebar(Rect r, const char *title, bool *close, v2 *move)
|
||||
{
|
||||
return gui_window_titlebar(&global_gui_state.default_context, r, title, close, move);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Gui_Window *gui_window_by_id(Gui_Context *ctx, Rect r, Gui_Id id)
|
||||
{
|
||||
Gui_Window *window = NULL;
|
||||
for(u32 i = 0; i < ctx->window_count; i++)
|
||||
{
|
||||
if(ctx->windows[i].id == id)
|
||||
{
|
||||
window = &ctx->windows[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!window)
|
||||
{
|
||||
if(ctx->window_count >= ctx->window_capacity)
|
||||
{
|
||||
if(ctx->window_capacity == 0)
|
||||
ctx->window_capacity = 1;
|
||||
ctx->window_capacity *= 2;
|
||||
ctx->windows = (Gui_Window*) p_realloc(ctx->windows, sizeof(Gui_Window) * ctx->window_capacity);
|
||||
}
|
||||
|
||||
window = &ctx->windows[ctx->window_count];
|
||||
ctx->window_count++;
|
||||
|
||||
window->id = id;
|
||||
window->r = r;
|
||||
window->framebuffer = r_framebuffer_create(V2S(r.size), 0);
|
||||
}
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
void gui_window_update_rect(Gui_Context *ctx, Gui_Window *window, Rect r)
|
||||
{
|
||||
if(window->r.size != r.size)
|
||||
{
|
||||
r_framebuffer_update_size(&window->framebuffer, V2S(r.size));
|
||||
}
|
||||
window->r = r;
|
||||
}
|
||||
|
||||
|
||||
// Helpers
|
||||
bool gui_is_hovered(Gui_Context *ctx, Gui_Id widget_id, Rect r)
|
||||
{
|
||||
if(is_inside(r, ctx->input.pointer_position))
|
||||
{
|
||||
s32 current_window_index = -1; // We use -1 to indicate we are not in a window. When we iterate over windows we do a +1 and start from 0, aka the first window. If we used 0, we would start from 1 and skip over window index 0.
|
||||
|
||||
// The ctx->windows array is sorted from back to front. If we are inside a window, only the following windows in the array can overlap up. The ones before are covered by the current window.
|
||||
if(ctx->current_window)
|
||||
current_window_index = ctx->current_window - ctx->windows;
|
||||
|
||||
// Am I a window? If so, we start checking from us. If ctx->current_window is set and widget_id is a window, it means we are a subwindow.
|
||||
// Subwindow are not supported yet though (20 September 2023), so this should be a bug in the user code. Yeah we don't check to prevent this, but anyways.
|
||||
for(s32 i = current_window_index + 1; i < ctx->window_count; i++)
|
||||
{
|
||||
Gui_Id window_id = ctx->windows[i].id;
|
||||
if(widget_id == window_id)
|
||||
{
|
||||
current_window_index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over windows that cover the current one
|
||||
for(u32 i = current_window_index + 1; i < ctx->window_count; i++)
|
||||
{
|
||||
Gui_Id window_id = ctx->windows[i].id;
|
||||
Rect window_rect = ctx->windows[i].r;
|
||||
if(is_inside(window_rect, ctx->input.absolute_pointer_position))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool gui_button_behaviuor(Gui_Context *ctx, Gui_Id widget_id, Rect r)
|
||||
{
|
||||
bool behaviour = false;
|
||||
if(gui_is_hovered(ctx, widget_id, r))
|
||||
{
|
||||
if(!ctx->active || ctx->active == widget_id || !(ctx->active_status & GUI_WIDGET_STATUS_PREVENT_HOT))
|
||||
ctx->possibly_hot = widget_id;
|
||||
|
||||
if(ctx->hot == widget_id && ctx->input.mouse_pressed_this_frame)
|
||||
{
|
||||
ctx->active = widget_id;
|
||||
ctx->active_start_time = ctx->current_frame_time;
|
||||
ctx->active_status = GUI_WIDGET_STATUS_PREVENT_HOT;
|
||||
}
|
||||
|
||||
if(ctx->active == widget_id && ctx->input.mouse_released_this_frame)
|
||||
{
|
||||
behaviour = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(ctx->active == widget_id && ctx->input.mouse_released_this_frame)
|
||||
{
|
||||
ctx->active = NULL;
|
||||
ctx->active_status = 0;
|
||||
}
|
||||
|
||||
return behaviour;
|
||||
}
|
||||
|
||||
|
||||
bool gui_text_input_behaviuor(Gui_Context *ctx, Gui_Id widget_id, Rect r)
|
||||
{
|
||||
bool behaviour = false;
|
||||
if(gui_is_hovered(ctx, widget_id, r))
|
||||
{
|
||||
if(!ctx->active || ctx->active == widget_id || !(ctx->active_status & GUI_WIDGET_STATUS_PREVENT_HOT))
|
||||
ctx->possibly_hot = widget_id;
|
||||
|
||||
if(ctx->hot == widget_id && ctx->input.mouse_pressed_this_frame)
|
||||
{
|
||||
ctx->active = widget_id;
|
||||
ctx->active_start_time = ctx->current_frame_time;
|
||||
ctx->active_status = 0;
|
||||
}
|
||||
|
||||
if(ctx->active == widget_id && ctx->input.mouse_released_this_frame)
|
||||
{
|
||||
behaviour = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(ctx->active == widget_id && ctx->input.mouse_released_this_frame)
|
||||
{
|
||||
// ctx->active = NULL;
|
||||
// ctx->active_status = 0;
|
||||
}
|
||||
|
||||
return behaviour;
|
||||
}
|
||||
|
||||
Gui_Id gui_id_from_pointer(Gui_Context *ctx, const void* ptr)
|
||||
{
|
||||
u32 seed = 0xFFFFFFFF;
|
||||
if(ctx->id_count)
|
||||
seed = ctx->id_stack[ctx->id_count - 1];
|
||||
return hash_crc32(&ptr, sizeof(void*), seed);
|
||||
}
|
||||
|
||||
void gui_id_stack_push(Gui_Context *ctx, Gui_Id id)
|
||||
{
|
||||
if(ctx->id_capacity <= ctx->id_count)
|
||||
{
|
||||
u32 new_capacity = maximum(ctx->id_count + 1, ctx->id_capacity * 2);
|
||||
ctx->id_stack = (Gui_Id*)p_realloc(ctx->id_stack, sizeof(Gui_Id) * new_capacity);
|
||||
ctx->id_capacity = new_capacity;
|
||||
}
|
||||
|
||||
ctx->id_stack[ctx->id_count] = id;
|
||||
ctx->id_count++;
|
||||
}
|
||||
|
||||
void gui_id_stack_pop(Gui_Context *ctx)
|
||||
{
|
||||
if(ctx->id_count > 0)
|
||||
ctx->id_count--;
|
||||
}
|
||||
195
code/gui/gui.h
Normal file
195
code/gui/gui.h
Normal file
@@ -0,0 +1,195 @@
|
||||
#ifndef _PIUMA_GUI_H_
|
||||
#define _PIUMA_GUI_H_
|
||||
|
||||
#include "../lib/types.h"
|
||||
#include "../lib/math.h"
|
||||
#include "../lib/geometry.h"
|
||||
#include "../render/2d.h"
|
||||
#include "../lib/text.h"
|
||||
#include "../lib/event.h"
|
||||
|
||||
|
||||
|
||||
typedef u32 Gui_Id;
|
||||
|
||||
|
||||
struct Gui_Input
|
||||
{
|
||||
v2 pointer_position;
|
||||
v2 absolute_pointer_position;
|
||||
bool mouse_pressed;
|
||||
bool mouse_pressed_this_frame;
|
||||
bool mouse_released_this_frame;
|
||||
s64 text_cursor_move;
|
||||
char text[32];
|
||||
|
||||
v2 absolute_pointer_position_last_frame;
|
||||
};
|
||||
|
||||
enum Gui_Text_Align
|
||||
{
|
||||
GUI_ALIGN_CENTER,
|
||||
GUI_ALIGN_LEFT,
|
||||
GUI_ALIGN_RIGHT
|
||||
};
|
||||
|
||||
struct Gui_Style
|
||||
{
|
||||
f32 font_size;
|
||||
f32 animation_base_time;
|
||||
|
||||
v4 text_color;
|
||||
Gui_Text_Align text_align;
|
||||
|
||||
v4 button_color;
|
||||
v4 button_color_hovered;
|
||||
v4 button_color_pressed;
|
||||
v4 button_text_color;
|
||||
v4 button_text_color_hovered;
|
||||
v4 button_text_color_pressed;
|
||||
f32 button_radius;
|
||||
|
||||
v4 slider_fill_color;
|
||||
|
||||
v4 window_background_color;
|
||||
v4 window_border_color;
|
||||
v4 window_background_color_inactive;
|
||||
v4 window_border_color_inactive;
|
||||
f32 window_corner_radius;
|
||||
v4 window_titlebar_color;
|
||||
v4 window_titlebar_color_inactive;
|
||||
};
|
||||
|
||||
struct Gui_Window
|
||||
{
|
||||
Gui_Id id;
|
||||
Rect r;
|
||||
r_framebuffer framebuffer;
|
||||
bool still_open;
|
||||
};
|
||||
|
||||
enum Gui_Widget_Status_Flags : u8
|
||||
{
|
||||
GUI_WIDGET_STATUS_NONE,
|
||||
GUI_WIDGET_STATUS_PREVENT_HOT
|
||||
};
|
||||
|
||||
struct Gui_Context
|
||||
{
|
||||
u32 width;
|
||||
u32 height;
|
||||
|
||||
f64 last_frame_time;
|
||||
f64 current_frame_time;
|
||||
|
||||
Gui_Id active; // Are we interacting with the widget (pressed for buttons, ready to receive text for text inputs)
|
||||
Gui_Id hot; // Hovered the previous frame
|
||||
Gui_Id possibly_hot; // Might become hot this frame
|
||||
f64 active_start_time;
|
||||
f64 hot_start_time;
|
||||
u8 active_status;
|
||||
|
||||
// Text input state
|
||||
u64 text_cursor_position;
|
||||
u64 text_length;
|
||||
|
||||
// Windows
|
||||
Gui_Window *windows;
|
||||
u32 window_count;
|
||||
u32 window_capacity;
|
||||
Gui_Window *current_window;
|
||||
r_framebuffer *old_framebuffer;
|
||||
|
||||
// ID
|
||||
Gui_Id *id_stack;
|
||||
u32 id_count;
|
||||
u32 id_capacity;
|
||||
|
||||
Gui_Input input;
|
||||
Gui_Style style;
|
||||
};
|
||||
|
||||
struct Gui_State
|
||||
{
|
||||
Gui_Context default_context;
|
||||
Gui_Context *selected_context;
|
||||
};
|
||||
|
||||
|
||||
extern Gui_State global_gui_state;
|
||||
|
||||
|
||||
|
||||
|
||||
bool gui_init();
|
||||
void gui_deinit();
|
||||
|
||||
void gui_context_init(Gui_Context *ctx);
|
||||
//@Correctness: gui_context_deinit
|
||||
|
||||
void gui_context_select(Gui_Context *ctx); // Set implicit Gui_Context
|
||||
|
||||
void gui_frame_begin(Gui_Context *ctx, f64 curr_time);
|
||||
void gui_frame_begin(f64 curr_time);
|
||||
void gui_frame_end(Gui_Context *ctx);
|
||||
void gui_frame_end();
|
||||
|
||||
void gui_handle_event(Gui_Context *ctx, Event *e);
|
||||
void gui_handle_event(Event *e);
|
||||
|
||||
// ### Widgets ###
|
||||
// Text
|
||||
void gui_text(Gui_Context *ctx, Rect r, const char *text);
|
||||
void gui_text(Rect r, const char *text);
|
||||
void gui_text_aligned(Gui_Context *ctx, Rect r, const char *text, Gui_Text_Align alignment);
|
||||
void gui_text_aligned(Rect r, const char *text, Gui_Text_Align alignment);
|
||||
|
||||
v2 gui_text_compute_size(Gui_Context *ctx, const char *text);
|
||||
v2 gui_text_compute_size(const char *text);
|
||||
|
||||
// Button
|
||||
bool gui_button(Gui_Context *ctx, Rect r, const char *text);
|
||||
bool gui_button(Rect r, const char *text);
|
||||
|
||||
// Slider
|
||||
bool gui_slider(Gui_Context *ctx, Rect r, f32 min, f32 max, f32 *value);
|
||||
bool gui_slider(Rect r, f32 min, f32 max, f32 *value);
|
||||
|
||||
// Checkbox
|
||||
// Option buttons
|
||||
// Combo box
|
||||
// Tooltips
|
||||
|
||||
// Images
|
||||
bool gui_image(Gui_Context *ctx, Rect r, r_texture *texture);
|
||||
bool gui_image(Rect r, r_texture *texture);
|
||||
bool gui_image(Gui_Context *ctx, Rect r, const u8 *bmp, u32 width, u32 height, u32 channels, u32 flags = 0);
|
||||
bool gui_image(Rect r, const u8 *bmp, u32 width, u32 height, u32 channels, u32 flags = 0);
|
||||
|
||||
// Text input
|
||||
bool gui_text_input(Gui_Context *ctx, Rect r, char *text, u64 max_size);
|
||||
bool gui_text_input(Rect r, char *text, u64 max_size);
|
||||
|
||||
// Windows
|
||||
bool gui_window_start(Gui_Context *ctx, Rect r, Gui_Id id); // You have to provide some kind of unique id to identify windows
|
||||
bool gui_window_start(Rect r, Gui_Id id);
|
||||
void gui_window_end(Gui_Context *ctx);
|
||||
void gui_window_end();
|
||||
|
||||
bool gui_window_titlebar(Gui_Context *ctx, Rect r, const char *title, bool *close, v2 *move);
|
||||
bool gui_window_titlebar(Rect r, const char *title, bool *close, v2 *move);
|
||||
|
||||
Gui_Window *gui_window_by_id(Gui_Context *ctx, Rect r, Gui_Id id); // Rect r might be needed for creation
|
||||
void gui_window_update_rect(Gui_Context *ctx, Gui_Window *window, Rect r);
|
||||
|
||||
// Helpers
|
||||
bool gui_is_hovered (Gui_Context *ctx, Gui_Id widget_id, Rect r);
|
||||
bool gui_button_behaviuor (Gui_Context *ctx, Gui_Id widget_id, Rect r);
|
||||
bool gui_text_input_behaviuor(Gui_Context *ctx, Gui_Id widget_id, Rect r);
|
||||
bool gui_window_behaviour (Gui_Context *ctx, Gui_Id widget_id, Rect r);
|
||||
|
||||
Gui_Id gui_id_from_pointer(Gui_Context *ctx, const void* ptr);
|
||||
void gui_id_stack_push(Gui_Context *ctx, Gui_Id id);
|
||||
void gui_id_stack_pop(Gui_Context *ctx);
|
||||
|
||||
#endif
|
||||
159
code/gui/layout.cpp
Normal file
159
code/gui/layout.cpp
Normal file
@@ -0,0 +1,159 @@
|
||||
#include "layout.h"
|
||||
#include "gui.h"
|
||||
|
||||
// Grid
|
||||
Rect Gui_Layout_Grid::rect()
|
||||
{
|
||||
return last_rect;
|
||||
}
|
||||
|
||||
void Gui_Layout_Grid::row(u32 count)
|
||||
{
|
||||
cursor.x = 0;
|
||||
cursor.y += count;
|
||||
}
|
||||
|
||||
Rect Gui_Layout_Grid::cell(u32 count)
|
||||
{
|
||||
count = minimum(count, max_cells_count.x);
|
||||
s32 free_space_in_row = max_cells_count.x - cursor.x;
|
||||
if(free_space_in_row < count)
|
||||
row();
|
||||
|
||||
last_rect = rect_at(cursor, {count, 1});
|
||||
cursor.x += count;
|
||||
|
||||
return last_rect;
|
||||
}
|
||||
|
||||
Rect Gui_Layout_Grid::rect_at(v2s cell_index, v2s size)
|
||||
{
|
||||
Rect result;
|
||||
v2 cell_border_v = v2{cell_border, cell_border};
|
||||
result.position = window_position + cell_border_v + (cell_border_v + cell_size) * V2(cell_index);
|
||||
result.size = cell_size * V2(size) + cell_border_v * (V2(size - v2s{1,1}));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Gui_Layout_Grid gui_layout_grid_create_by_divisions(v2 position, v2 window_size, u32 divisions_x, u32 divisions_y, f32 border)
|
||||
{
|
||||
Gui_Layout_Grid layout;
|
||||
layout.window_position = position;
|
||||
layout.window_size = window_size;
|
||||
layout.cell_size = (window_size - v2{1,1}*border) / v2{divisions_x, divisions_y} - v2{1,1}*border;
|
||||
layout.cell_border = border;
|
||||
layout.max_cells_count = v2s{divisions_x, divisions_y};
|
||||
|
||||
layout.cursor = {0,0};
|
||||
layout.last_rect = layout.rect_at({0,0});
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
Gui_Layout_Grid gui_layout_grid_create_by_cell_size(v2 position, v2 window_size, v2 cell_size, f32 border)
|
||||
{
|
||||
Gui_Layout_Grid layout;
|
||||
layout.window_position = position;
|
||||
layout.window_size = window_size;
|
||||
layout.cell_size = cell_size;
|
||||
layout.cell_border = border;
|
||||
layout.max_cells_count = V2S((window_size - v2{1,1}*border) / (cell_size + v2{1,1}*border));
|
||||
|
||||
layout.cursor = {0,0};
|
||||
layout.last_rect = layout.rect_at({0,0});
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
|
||||
// Basic
|
||||
Rect Gui_Layout_Basic::rect()
|
||||
{
|
||||
return last_rect;
|
||||
}
|
||||
|
||||
void Gui_Layout_Basic::push_rect(Rect r)
|
||||
{
|
||||
cursor.x = cursor.x + r.size.x + element_distance.x;
|
||||
cursor.y = cursor.y;
|
||||
|
||||
last_rect = r;
|
||||
next_row_y = maximum(next_row_y, cursor.y + r.size.y + element_distance.y);
|
||||
}
|
||||
|
||||
void Gui_Layout_Basic::row()
|
||||
{
|
||||
cursor.x = window_padding.x;
|
||||
cursor.y = next_row_y;
|
||||
next_row_y = cursor.y + font_size + element_distance.y;
|
||||
}
|
||||
|
||||
Rect Gui_Layout_Basic::label_rect(const char *text)
|
||||
{
|
||||
Rect r;
|
||||
r.size.y = font_size;
|
||||
r.size.x = gui_text_compute_size(text).x;
|
||||
|
||||
f32 remaining_space_x = window_size.x - 2*window_padding.x - cursor.x;
|
||||
if(cursor.x != window_padding.x && remaining_space_x < r.size.x)
|
||||
row();
|
||||
|
||||
r.position = window_position + cursor;
|
||||
|
||||
push_rect(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
Rect Gui_Layout_Basic::button_rect(const char *text)
|
||||
{
|
||||
Rect r;
|
||||
r.size.y = font_size;
|
||||
r.size.x = gui_text_compute_size(text).x;
|
||||
r.size += 2*button_padding;
|
||||
|
||||
f32 remaining_space_x = window_size.x - 2*window_padding.x - cursor.x;
|
||||
if(cursor.x != window_padding.x && remaining_space_x < r.size.x)
|
||||
row();
|
||||
|
||||
r.position = window_position + cursor;
|
||||
|
||||
push_rect(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
Rect Gui_Layout_Basic::button_rect(f32 length)
|
||||
{
|
||||
Rect r;
|
||||
r.size.y = font_size;
|
||||
r.size.x = length;
|
||||
r.size.y += 2*button_padding.y; // No x padding in this case. We already have an assigned length
|
||||
|
||||
f32 remaining_space_x = window_size.x - 2*window_padding.x - cursor.x;
|
||||
if(cursor.x != window_padding.x && remaining_space_x < r.size.x)
|
||||
row();
|
||||
|
||||
r.position = window_position + cursor;
|
||||
|
||||
push_rect(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
Gui_Layout_Basic gui_layout_basic_create(v2 position, v2 size, f32 font_size)
|
||||
{
|
||||
Gui_Layout_Basic layout;
|
||||
layout.window_position = position;
|
||||
layout.window_size = size;
|
||||
|
||||
layout.cursor = {0,0};
|
||||
layout.next_row_y = 0;
|
||||
layout.last_rect = {0,0,0,0};
|
||||
|
||||
layout.font_size = font_size;
|
||||
layout.button_padding = {0,0};
|
||||
layout.element_distance = {0,0};
|
||||
layout.window_padding = {0,0};
|
||||
|
||||
return layout;
|
||||
}
|
||||
57
code/gui/layout.h
Normal file
57
code/gui/layout.h
Normal file
@@ -0,0 +1,57 @@
|
||||
#ifndef _PIUMA_GUI_LAYOUT_H_
|
||||
#define _PIUMA_GUI_LAYOUT_H_
|
||||
|
||||
#include "../lib/types.h"
|
||||
#include "../lib/geometry.h"
|
||||
|
||||
// Grid
|
||||
const s32 GUI_LAYOUT_MAX_CELLS = 0x7FFFFFFF;
|
||||
|
||||
struct Gui_Layout_Grid
|
||||
{
|
||||
v2 window_position;
|
||||
v2 window_size;
|
||||
v2 cell_size;
|
||||
f32 cell_border;
|
||||
v2s max_cells_count;
|
||||
|
||||
v2s cursor;
|
||||
Rect last_rect;
|
||||
|
||||
Rect rect(); // Get last rect
|
||||
void row(u32 count = 1);
|
||||
Rect cell(u32 count = 1);
|
||||
Rect rect_at(v2s position, v2s size = {1,1}); // Does not modify cursor. You have to assign it yourself.
|
||||
|
||||
};
|
||||
|
||||
Gui_Layout_Grid gui_layout_grid_create_by_divisions(v2 position, v2 window_size, u32 divisions_x, u32 divisions_y, f32 border = 0);
|
||||
Gui_Layout_Grid gui_layout_grid_create_by_cell_size(v2 position, v2 window_size, v2 cell_size, f32 border = 0);
|
||||
|
||||
|
||||
// Basic
|
||||
struct Gui_Layout_Basic
|
||||
{
|
||||
v2 window_position;
|
||||
v2 window_size;
|
||||
|
||||
v2 cursor = {0,0};
|
||||
f32 next_row_y = 0;
|
||||
Rect last_rect = {0,0,0,0};
|
||||
|
||||
f32 font_size;
|
||||
v2 button_padding = {0,0};
|
||||
v2 element_distance = {0,0};
|
||||
v2 window_padding = {0,0};
|
||||
|
||||
Rect rect();
|
||||
void push_rect(Rect r);
|
||||
void row();
|
||||
Rect label_rect(const char *text = "");
|
||||
Rect button_rect(const char *text = "");
|
||||
Rect button_rect(f32 length);
|
||||
};
|
||||
|
||||
Gui_Layout_Basic gui_layout_basic_create(v2 position, v2 size, f32 font_size);
|
||||
|
||||
#endif
|
||||
548
code/gui/text_draw.cpp
Normal file
548
code/gui/text_draw.cpp
Normal file
@@ -0,0 +1,548 @@
|
||||
#include "text_draw.h"
|
||||
#include "../lib/color.h"
|
||||
#include "../lib/math.h"
|
||||
#include "../lib/geometry.h"
|
||||
#include "../lib/text.h"
|
||||
#include "../lib/ds.h"
|
||||
#include "../platform.h"
|
||||
#include "stb_truetype.h"
|
||||
#include "../debug/logger.h"
|
||||
#include "../assets.h"
|
||||
#include "../enginestate.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
|
||||
|
||||
struct gui_glyph_info
|
||||
{
|
||||
utf8_codepoint codepoint;
|
||||
Rect box; // .position = offset from position for alignment, .size = size of the bitmap to draw
|
||||
v2u position; // Position of the top left border in the texture
|
||||
s32 advance;
|
||||
u32 next; // Anything >= container_capacity is to be considered NULL
|
||||
u32 previous;
|
||||
};
|
||||
|
||||
|
||||
/* Glyph caching:
|
||||
* We store a small number of sizes and a fairly large number of characters for each size.
|
||||
* Each slot will have the same width and height, so that we can easily replace it without
|
||||
* re-packing everything. This will also make the code simpler.
|
||||
* We use 0xFFFFFFFF as a placeholder for empty slots.
|
||||
*
|
||||
*/
|
||||
|
||||
struct gui_glyph_codepoint_map
|
||||
{
|
||||
utf8_codepoint codepoint;
|
||||
u32 index;
|
||||
};
|
||||
|
||||
struct gui_glyph_texture
|
||||
{
|
||||
f32 font_size;
|
||||
|
||||
struct gui_glyph_codepoint_map *sorted_indices;
|
||||
gui_glyph_info *info;
|
||||
u32 oldest;
|
||||
u32 newest;
|
||||
u32 capacity;
|
||||
|
||||
v2s glyph_max_size;
|
||||
|
||||
r_texture *texture;
|
||||
};
|
||||
|
||||
struct gui_glyph_cache
|
||||
{
|
||||
gui_glyph_texture *glyphs;
|
||||
u32 capacity;
|
||||
u32 oldest;
|
||||
|
||||
u32 max_glyphs_per_texture;
|
||||
};
|
||||
|
||||
static void gui_glyph_cache_init();
|
||||
static void gui_glyph_cache_deinit();
|
||||
static gui_glyph_texture gui_glyph_texture_create(f32 font_size, u32 capacity);
|
||||
static void gui_glyph_texture_destroy(gui_glyph_texture *glyphs);
|
||||
static gui_glyph_texture *gui_glyph_cache_texture_for_codepoints(f32 font_size, const utf8_codepoint *codepoints, u64 count);
|
||||
|
||||
|
||||
|
||||
|
||||
// Globals
|
||||
static u8 *Font_File;
|
||||
static stbtt_fontinfo Font_Info;
|
||||
static gui_glyph_cache Glyph_Cache;
|
||||
|
||||
|
||||
bool gui_text_draw_init()
|
||||
{
|
||||
// @Feature: user specified font
|
||||
// @Cleanup: one-line file read
|
||||
p_file f;
|
||||
p_file_init(&f, "assets/fonts/DejaVuSerif-Bold.ttf");
|
||||
Buffer buf;
|
||||
buf.size = p_file_size(&f);
|
||||
buf.data = (u8*)p_alloc(buf.size + 1);
|
||||
buf.data[buf.size] = '\0';
|
||||
p_file_read(&f, &buf, buf.size);
|
||||
p_file_deinit(&f);
|
||||
Font_File = buf.data;
|
||||
|
||||
if(!stbtt_InitFont(&Font_Info, Font_File, 0))
|
||||
{
|
||||
LOG(LOG_ERROR, "Cannot load font.");
|
||||
return false;
|
||||
}
|
||||
|
||||
gui_glyph_cache_init();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void gui_text_draw_deinit()
|
||||
{
|
||||
gui_glyph_cache_deinit();
|
||||
p_free(Font_File);
|
||||
}
|
||||
|
||||
v2 gui_text_draw_size(const char *text, f32 font_size, v2 *cursor_position)
|
||||
{
|
||||
// UTF8 conversion
|
||||
u64 text_length = utf8_codepoint_count(text);
|
||||
utf8_codepoint *codepoints = (utf8_codepoint*) p_alloc(text_length * sizeof(utf8_codepoint));
|
||||
{
|
||||
u64 bytes_read = 0;
|
||||
text_length = utf8_from_string(text, &bytes_read, codepoints, text_length);
|
||||
}
|
||||
|
||||
// Compute size
|
||||
v2 result = gui_utf8_text_draw_size(codepoints, text_length, font_size, cursor_position);
|
||||
|
||||
p_free(codepoints);
|
||||
return result;
|
||||
}
|
||||
|
||||
void gui_text_draw(Rect r, const char *text, f32 font_size, v4 color)
|
||||
{
|
||||
// UTF8 conversion
|
||||
u64 text_length = utf8_codepoint_count(text);
|
||||
utf8_codepoint *codepoints = (utf8_codepoint*) p_alloc(text_length * sizeof(utf8_codepoint));
|
||||
{
|
||||
u64 bytes_read = 0;
|
||||
text_length = utf8_from_string(text, &bytes_read, codepoints, text_length);
|
||||
}
|
||||
|
||||
// Draw
|
||||
gui_utf8_text_draw(r, codepoints, text_length, font_size, color);
|
||||
|
||||
p_free(codepoints);
|
||||
}
|
||||
|
||||
|
||||
v2 gui_utf8_text_draw_size(const utf8_codepoint *text, u64 length, f32 font_size, v2 *cursor_position)
|
||||
{
|
||||
f32 font_scale;
|
||||
s32 font_ascent;
|
||||
s32 font_descent;
|
||||
s32 font_line_gap;
|
||||
s32 font_baseline;
|
||||
|
||||
{
|
||||
font_scale = stbtt_ScaleForPixelHeight(&Font_Info, font_size);
|
||||
stbtt_GetFontVMetrics(&Font_Info, &font_ascent, &font_descent, &font_line_gap);
|
||||
font_baseline = font_ascent;
|
||||
|
||||
font_ascent *= font_scale;
|
||||
font_descent *= font_scale;
|
||||
font_line_gap *= font_scale;
|
||||
font_baseline *= font_scale;
|
||||
}
|
||||
|
||||
// Compute size
|
||||
v2 size = {0, (f32)(font_ascent - font_descent)};
|
||||
{
|
||||
v2 cursor = {0, (f32)(font_ascent - font_descent)};
|
||||
for(u64 i = 0; i < length; i++)
|
||||
{
|
||||
s32 advance, lsb;
|
||||
stbtt_GetCodepointHMetrics(&Font_Info, text[i], &advance, &lsb);
|
||||
|
||||
// Special characters
|
||||
if(text[i] == 10) // '\n'
|
||||
{
|
||||
advance = 0;
|
||||
cursor.x = 0;
|
||||
cursor.y += font_ascent - font_descent + font_line_gap;
|
||||
size.y = cursor.y;
|
||||
}
|
||||
|
||||
// Normal characters
|
||||
cursor.x += floor(advance * font_scale); // Remember to consider kerning
|
||||
size.x = maximum(size.x, cursor.x);
|
||||
}
|
||||
if(cursor_position)
|
||||
*cursor_position = cursor;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
void gui_utf8_text_draw(Rect r, const utf8_codepoint *text, u64 length, f32 font_size, v4 color)
|
||||
{
|
||||
f32 font_scale;
|
||||
s32 font_ascent;
|
||||
s32 font_descent;
|
||||
s32 font_line_gap;
|
||||
s32 font_baseline;
|
||||
|
||||
{
|
||||
font_scale = stbtt_ScaleForPixelHeight(&Font_Info, font_size);
|
||||
stbtt_GetFontVMetrics(&Font_Info, &font_ascent, &font_descent, &font_line_gap);
|
||||
font_baseline = font_ascent;
|
||||
|
||||
font_ascent *= font_scale;
|
||||
font_descent *= font_scale;
|
||||
font_line_gap *= font_scale;
|
||||
font_baseline *= font_scale;
|
||||
}
|
||||
|
||||
// Compute glyphs
|
||||
gui_glyph_texture *glyphs = gui_glyph_cache_texture_for_codepoints(font_size, text, length);
|
||||
|
||||
// Map text to quads
|
||||
v2 *vertices;
|
||||
v2 *uvs;
|
||||
u64 draw_count = 0;
|
||||
{
|
||||
vertices = (v2*) p_alloc(6 * length * sizeof(v2)); // 2 triangles, 3 vertices each = 6 vertices
|
||||
uvs = (v2*) p_alloc(6 * length * sizeof(v2));
|
||||
|
||||
|
||||
v2 position = r.position;
|
||||
position.y += font_baseline;
|
||||
for(u64 i = 0; i < length; i++)
|
||||
{
|
||||
gui_glyph_codepoint_map *found = (gui_glyph_codepoint_map*) bsearch(&text[i], glyphs->sorted_indices, glyphs->capacity, sizeof(gui_glyph_codepoint_map), u32_cmp);
|
||||
if(found == NULL)
|
||||
{
|
||||
LOG(LOG_ERROR, "Cannot find codepoint 0x%X in glyph list.", text[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
u64 glyph_i = glyphs->sorted_indices[found - glyphs->sorted_indices].index;
|
||||
gui_glyph_info *info = &glyphs->info[glyph_i];
|
||||
|
||||
// Special characters
|
||||
if(text[i] == 10) // '\n'
|
||||
{
|
||||
position.x = r.position.x;
|
||||
position.y += font_ascent - font_descent + font_line_gap;
|
||||
}
|
||||
|
||||
// Normal characters
|
||||
// Map character to its vertices
|
||||
{
|
||||
Rect r;
|
||||
Rect r_uv;
|
||||
r.position = position + info->box.position;
|
||||
r.position.x = floor(r.position.x + 0.5);
|
||||
r.position.y = floor(r.position.y + 0.5);
|
||||
r.size = info->box.size;
|
||||
|
||||
r_uv.position = V2(info->position) / V2(glyphs->texture->size);
|
||||
r_uv.size = info->box.size / V2(glyphs->texture->size);
|
||||
|
||||
v2 a = r.position + v2{r.w, 0 };
|
||||
v2 b = r.position + v2{0 , 0 };
|
||||
v2 c = r.position + v2{0 , r.h};
|
||||
v2 d = r.position + v2{r.w, r.h};
|
||||
v2 a_uv = r_uv.position + v2{r_uv.w, 0 };
|
||||
v2 b_uv = r_uv.position + v2{0 , 0 };
|
||||
v2 c_uv = r_uv.position + v2{0 , r_uv.h};
|
||||
v2 d_uv = r_uv.position + v2{r_uv.w, r_uv.h};
|
||||
|
||||
vertices[6*draw_count + 0] = a;
|
||||
vertices[6*draw_count + 1] = b;
|
||||
vertices[6*draw_count + 2] = c;
|
||||
vertices[6*draw_count + 3] = a;
|
||||
vertices[6*draw_count + 4] = c;
|
||||
vertices[6*draw_count + 5] = d;
|
||||
uvs[6*draw_count + 0] = a_uv;
|
||||
uvs[6*draw_count + 1] = b_uv;
|
||||
uvs[6*draw_count + 2] = c_uv;
|
||||
uvs[6*draw_count + 3] = a_uv;
|
||||
uvs[6*draw_count + 4] = c_uv;
|
||||
uvs[6*draw_count + 5] = d_uv;
|
||||
}
|
||||
|
||||
|
||||
position.x += info->advance; // Remember to consider kerning
|
||||
draw_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Render quads
|
||||
r_2d_immediate_mesh(6*draw_count, vertices, color, uvs, glyphs->texture);
|
||||
|
||||
p_free(vertices);
|
||||
p_free(uvs);
|
||||
}
|
||||
|
||||
|
||||
static void gui_glyph_cache_init()
|
||||
{
|
||||
Glyph_Cache.capacity = 4;
|
||||
Glyph_Cache.oldest = 0;
|
||||
Glyph_Cache.glyphs = (gui_glyph_texture*)p_alloc(sizeof(gui_glyph_texture) * Glyph_Cache.capacity);
|
||||
memset(Glyph_Cache.glyphs, 0, sizeof(gui_glyph_texture) * Glyph_Cache.capacity);
|
||||
Glyph_Cache.max_glyphs_per_texture = 256; // @Correctness: test with small values, to trigger the glyph replacement code
|
||||
}
|
||||
|
||||
static void gui_glyph_cache_deinit()
|
||||
{
|
||||
for(u32 i = 0; i < Glyph_Cache.capacity; i++)
|
||||
gui_glyph_texture_destroy(&Glyph_Cache.glyphs[i]);
|
||||
p_free(Glyph_Cache.glyphs);
|
||||
Glyph_Cache.glyphs = NULL;
|
||||
Glyph_Cache.capacity = 0;
|
||||
Glyph_Cache.oldest = 0;
|
||||
}
|
||||
|
||||
static gui_glyph_texture gui_glyph_texture_create(f32 font_size, u32 capacity)
|
||||
{
|
||||
// Init container for glyphs and info
|
||||
gui_glyph_texture glyphs;
|
||||
glyphs.font_size = font_size;
|
||||
glyphs.sorted_indices = (gui_glyph_codepoint_map*) p_alloc(sizeof(gui_glyph_codepoint_map) * capacity);
|
||||
glyphs.info = (gui_glyph_info*) p_alloc(sizeof(gui_glyph_info) * capacity);
|
||||
glyphs.oldest = 0;
|
||||
glyphs.newest = capacity - 1;
|
||||
glyphs.capacity = capacity;
|
||||
|
||||
// Estimate max glyph size.
|
||||
// @Correctness: Text draw will fail if a bigger glyph is used.
|
||||
f32 font_scale = stbtt_ScaleForPixelHeight(&Font_Info, font_size);
|
||||
utf8_codepoint cp[] = {' ', 'M', 'j', '{', '=', 'w', 'W'};
|
||||
glyphs.glyph_max_size = v2s{0, 0};
|
||||
for(s32 i = 0; i < sizeof(cp)/sizeof(utf8_codepoint); i++)
|
||||
{
|
||||
v2s top_left, bottom_right;
|
||||
stbtt_GetCodepointBitmapBox(&Font_Info, cp[i], font_scale, font_scale, &top_left.x, &top_left.y, &bottom_right.x, &bottom_right.y);
|
||||
v2s size = bottom_right - top_left;
|
||||
|
||||
glyphs.glyph_max_size.x = maximum(glyphs.glyph_max_size.x, size.x);
|
||||
glyphs.glyph_max_size.y = maximum(glyphs.glyph_max_size.y, size.y);
|
||||
}
|
||||
LOG(LOG_DEBUG, "Font size %f not in cache. Slot size (%d %d)", font_size, glyphs.glyph_max_size.x, glyphs.glyph_max_size.y);
|
||||
|
||||
// Precompile some info data
|
||||
for(u32 i = 0; i < glyphs.capacity; i++)
|
||||
{
|
||||
glyphs.info[i] = gui_glyph_info{
|
||||
.codepoint = 0xFFFFFFFF,
|
||||
.box = {0,0,0,0},
|
||||
.position = v2u{glyphs.glyph_max_size.x * i, 0},
|
||||
.advance = 0,
|
||||
.next = i + 1, // Last .next will be >= capacity (== capacity to be precise), so we will consider it to be NULL
|
||||
.previous = ((i == 0) ? glyphs.capacity : (i - 1))
|
||||
};
|
||||
|
||||
glyphs.sorted_indices[i] = gui_glyph_codepoint_map{0xFFFFFFFF, i};
|
||||
}
|
||||
|
||||
// Initialize texture
|
||||
v2s texture_size = v2s{glyphs.glyph_max_size.x * glyphs.capacity, glyphs.glyph_max_size.y};
|
||||
u8 *texture_data = (u8*)p_alloc(sizeof(u8) * texture_size.x * texture_size.y);
|
||||
memset(texture_data, 0, sizeof(u8) * texture_size.x * texture_size.y);
|
||||
|
||||
glyphs.texture = assets_new_textures(&engine.am, 1);
|
||||
*glyphs.texture = r_texture_create(texture_data, texture_size, R_TEXTURE_ALPHA | R_TEXTURE_NO_MIPMAP);
|
||||
|
||||
return glyphs;
|
||||
}
|
||||
|
||||
static void gui_glyph_texture_destroy(gui_glyph_texture *glyphs)
|
||||
{
|
||||
if(glyphs->sorted_indices)
|
||||
p_free(glyphs->sorted_indices);
|
||||
if(glyphs->info)
|
||||
p_free(glyphs->info);
|
||||
if(glyphs->texture)
|
||||
r_texture_destroy(glyphs->texture);
|
||||
glyphs->sorted_indices = NULL;
|
||||
glyphs->info = NULL;
|
||||
glyphs->texture = NULL;
|
||||
|
||||
glyphs->font_size = 0;
|
||||
glyphs->oldest = 0;
|
||||
glyphs->newest = 0;
|
||||
glyphs->capacity = 0;
|
||||
}
|
||||
|
||||
static gui_glyph_texture *gui_glyph_cache_texture_for_codepoints(f32 font_size, const utf8_codepoint *codepoints, u64 count)
|
||||
{
|
||||
// Approximate font_size. We don't really want to build different bitmaps for size 12.000000 and 12.000001.
|
||||
// This will also prevent floating point rounding errors from rebuilding the cache.
|
||||
font_size = floor(font_size * 10) / 10;
|
||||
|
||||
// Find cached texture for this size or build a new one
|
||||
gui_glyph_texture *glyphs = NULL;
|
||||
for(u32 i = 0; i < Glyph_Cache.capacity; i++)
|
||||
{
|
||||
//LOG(LOG_DEBUG, "Font size: %f - Cached: %f", font_size, Glyph_Cache.glyphs[i].font_size);
|
||||
if(abs(Glyph_Cache.glyphs[i].font_size - font_size) < 0.01)
|
||||
{
|
||||
glyphs = &Glyph_Cache.glyphs[i];
|
||||
}
|
||||
}
|
||||
if(glyphs == NULL)
|
||||
{
|
||||
//LOG(LOG_DEBUG, "Size not matched");
|
||||
glyphs = &Glyph_Cache.glyphs[Glyph_Cache.oldest];
|
||||
Glyph_Cache.oldest = (Glyph_Cache.oldest + 1) % Glyph_Cache.capacity;
|
||||
|
||||
gui_glyph_texture_destroy(glyphs);
|
||||
*glyphs = gui_glyph_texture_create(font_size, Glyph_Cache.max_glyphs_per_texture);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Build list of unique codepoints (so that we don't render the same codepoint twice)
|
||||
utf8_codepoint *unique = (utf8_codepoint*) p_alloc(count * sizeof(utf8_codepoint));
|
||||
memcpy(unique, codepoints, count * sizeof(utf8_codepoint));
|
||||
u64 unique_count = make_unique(unique, count, sizeof(utf8_codepoint), u32_cmp);
|
||||
if(unique_count > glyphs->capacity)
|
||||
LOG(LOG_ERROR, "Unique codepoints count > cache capacity. Some codepoints will not be rendered.");
|
||||
|
||||
// Find which codepoints are not already in the cache and need to be rendered
|
||||
utf8_codepoint to_render[unique_count];
|
||||
u32 to_render_count = 0;
|
||||
for(u32 i = 0; i < unique_count; i++)
|
||||
{
|
||||
gui_glyph_codepoint_map *found = (gui_glyph_codepoint_map*) bsearch(&unique[i], glyphs->sorted_indices, glyphs->capacity, sizeof(gui_glyph_codepoint_map), u32_cmp);
|
||||
if(found == NULL)
|
||||
{
|
||||
// Not found -> add to the list of glyphs to render
|
||||
to_render[to_render_count] = unique[i];
|
||||
to_render_count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Found -> mark it as recent, so that is does not get deleted prematurely
|
||||
u32 index = glyphs->sorted_indices[found - glyphs->sorted_indices].index;
|
||||
if(index == glyphs->newest)
|
||||
{
|
||||
// Already the newest. Do nothing.
|
||||
}
|
||||
else if(index == glyphs->oldest)
|
||||
{
|
||||
// We have no previous to fix, only next
|
||||
u32 next = glyphs->info[index].next;
|
||||
glyphs->info[next].previous = glyphs->capacity; // Next is the new oldest -> no previous
|
||||
glyphs->oldest = next;
|
||||
|
||||
// Set index as last element
|
||||
glyphs->info[index].next = glyphs->capacity;
|
||||
glyphs->info[index].previous = glyphs->newest;
|
||||
glyphs->info[glyphs->newest].next = index;
|
||||
glyphs->newest = index;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We in between the list. We have both previous and next elements to fix.
|
||||
u32 previous = glyphs->info[index].previous;
|
||||
u32 next = glyphs->info[index].next;
|
||||
glyphs->info[previous].next = next;
|
||||
glyphs->info[next].previous = previous;
|
||||
|
||||
// Set index as last element
|
||||
glyphs->info[index].next = glyphs->capacity;
|
||||
glyphs->info[index].previous = glyphs->newest;
|
||||
glyphs->info[glyphs->newest].next = index;
|
||||
glyphs->newest = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p_free(unique);
|
||||
|
||||
|
||||
// Get info for rendering
|
||||
f32 font_scale;
|
||||
s32 font_ascent;
|
||||
s32 font_descent;
|
||||
s32 font_line_gap;
|
||||
s32 font_baseline;
|
||||
{
|
||||
font_scale = stbtt_ScaleForPixelHeight(&Font_Info, font_size);
|
||||
stbtt_GetFontVMetrics(&Font_Info, &font_ascent, &font_descent, &font_line_gap);
|
||||
font_baseline = font_ascent;
|
||||
|
||||
font_ascent *= font_scale;
|
||||
font_descent *= font_scale;
|
||||
font_line_gap *= font_scale;
|
||||
font_baseline *= font_scale;
|
||||
}
|
||||
|
||||
// Render glyph in its appropriate place
|
||||
for(u32 i = 0; i < to_render_count; i++)
|
||||
{
|
||||
u32 index = glyphs->oldest;
|
||||
glyphs->oldest = glyphs->info[index].next;
|
||||
glyphs->info[glyphs->oldest].previous = glyphs->capacity;
|
||||
|
||||
glyphs->info[index].next = glyphs->capacity;
|
||||
glyphs->info[index].previous = glyphs->newest;
|
||||
glyphs->info[index].codepoint = to_render[i];
|
||||
|
||||
glyphs->info[glyphs->newest].next = index;
|
||||
glyphs->newest = index;
|
||||
|
||||
|
||||
// Complete gui_glyph_info structure and render
|
||||
gui_glyph_info *info = &glyphs->info[index];
|
||||
|
||||
v2s top_left, bottom_right;
|
||||
stbtt_GetCodepointBitmapBox(&Font_Info, info->codepoint, font_scale, font_scale, &top_left.x, &top_left.y, &bottom_right.x, &bottom_right.y);
|
||||
s32 advance, lsb;
|
||||
stbtt_GetCodepointHMetrics(&Font_Info, info->codepoint, &advance, &lsb);
|
||||
|
||||
v2s size = bottom_right - top_left;
|
||||
|
||||
// Special codepoints
|
||||
if(info->codepoint == 10) // '\n'
|
||||
{
|
||||
size = {0, 0};
|
||||
advance = 0;
|
||||
}
|
||||
|
||||
info->box.position = V2(top_left);
|
||||
info->box.size = V2(size);
|
||||
//info->position = v2u{glyphs->glyph_max_size.x * index, 0}; // Commented because it's already pre-computed.
|
||||
info->advance = advance * font_scale;
|
||||
|
||||
// @Correctness: needs to be premultiplied alpha
|
||||
stbtt_MakeCodepointBitmap(&Font_Info, glyphs->texture->data + info->position.x, info->box.size.x, info->box.size.y, glyphs->texture->size.x, font_scale, font_scale, info->codepoint);
|
||||
r_texture_update(glyphs->texture, glyphs->texture->data + info->position.x, V2S(info->box.size), V2S(info->position), glyphs->texture->size.x);
|
||||
}
|
||||
|
||||
// Build sorted array with indices that point to the element
|
||||
u32 nonempty_count = glyphs->capacity;
|
||||
for(u32 i = 0; i < glyphs->capacity; i++)
|
||||
{
|
||||
glyphs->sorted_indices[i] = gui_glyph_codepoint_map{glyphs->info[i].codepoint, i};
|
||||
if(glyphs->info[i].codepoint == 0xFFFFFFFF)
|
||||
{
|
||||
// When the cache is mostly empty, this makes the sorting way faster.
|
||||
nonempty_count = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
qsort(glyphs->sorted_indices, nonempty_count, sizeof(gui_glyph_codepoint_map), u32_cmp);
|
||||
|
||||
return glyphs;
|
||||
}
|
||||
18
code/gui/text_draw.h
Normal file
18
code/gui/text_draw.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#ifndef _PIUMA_GUI_TEXT_DRAW_H_
|
||||
#define _PIUMA_GUI_TEXT_DRAW_H_
|
||||
|
||||
#include "../lib/types.h"
|
||||
#include "../lib/math.h"
|
||||
#include "../lib/geometry.h"
|
||||
#include "../lib/text.h"
|
||||
|
||||
bool gui_text_draw_init();
|
||||
void gui_text_draw_deinit();
|
||||
|
||||
v2 gui_text_draw_size(const char *text, f32 font_size, v2 *cursor_position = NULL);
|
||||
void gui_text_draw(Rect r, const char *text, f32 font_size, v4 color);
|
||||
v2 gui_utf8_text_draw_size(const utf8_codepoint *text, u64 length, f32 font_size, v2 *cursor_position = NULL);
|
||||
void gui_utf8_text_draw(Rect r, const utf8_codepoint *text, u64 length, f32 font_size, v4 color);
|
||||
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user