1073 lines
32 KiB
C++
1073 lines
32 KiB
C++
#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 = 0;
|
||
ctx-> hot = 0;
|
||
ctx->possibly_hot = 0;
|
||
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.reserve(2);
|
||
ctx->current_window = NULL;
|
||
|
||
ctx->clipping.reserve();
|
||
|
||
ctx->id_stack.reserve();
|
||
|
||
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.scroll_move = 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};
|
||
|
||
ctx->style.scrollbar_size = 10;
|
||
ctx->style.scrollbar_corner_radius = 4;
|
||
ctx->style.scrollbar_inner_bar_size = 10;
|
||
ctx->style.scrollbar_inner_bar_corner_radius = 4;
|
||
ctx->style.scrollbar_color = v4{.1f,.1f,.1f,1.0f}*0.2f;
|
||
ctx->style.scrollbar_inner_bar_color = v4{.5f,.5f,.5f,1.0f};
|
||
}
|
||
|
||
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->windows.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->input.scroll_move = 0;
|
||
|
||
ctx->possibly_hot = 0;
|
||
}
|
||
|
||
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_MOUSE_WHEEL_UP: {
|
||
if(e->key.pressed)
|
||
ctx->input.scroll_move--;
|
||
} break;
|
||
case KEY_MOUSE_WHEEL_DOWN: {
|
||
if(e->key.pressed)
|
||
ctx->input.scroll_move++;
|
||
} 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)
|
||
{
|
||
if(gui_is_clipped(ctx, r)) return;
|
||
// @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)
|
||
{
|
||
if(gui_is_clipped(ctx, r)) return;
|
||
// @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)
|
||
{
|
||
if(gui_is_clipped(ctx, r)) return false;
|
||
|
||
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_range(Gui_Context *ctx, Rect r, f32 min, f32 max, f32 *value)
|
||
{
|
||
if(gui_is_clipped(ctx, r)) return false;
|
||
|
||
Gui_Id widget_id = gui_id_from_pointer(ctx, value);
|
||
gui_id_stack_push(ctx, widget_id);
|
||
// Value text
|
||
char text[64];
|
||
snprintf(text, 64, "%f", *value);
|
||
|
||
// Convert value from min-max to 0-1 range
|
||
f32 ratio = (*value - min) / (max - min);
|
||
|
||
// Do slider
|
||
bool behaviour = gui_slider_text(ctx, r, &ratio, text);
|
||
|
||
// Re-convert value from 0-1 to min-max range
|
||
*value = clamp(0.0f, 1.0f, ratio) * (max - min) + min;
|
||
|
||
gui_id_stack_pop(ctx);
|
||
return behaviour;
|
||
}
|
||
|
||
bool gui_slider_range(Rect r, f32 min, f32 max, f32 *value)
|
||
{
|
||
return gui_slider_range(&global_gui_state.default_context, r, min, max, value);
|
||
}
|
||
|
||
// ratio must be between 0 and 1.
|
||
bool gui_slider_text(Gui_Context *ctx, Rect r, f32 *ratio, const char *text)
|
||
{
|
||
if(gui_is_clipped(ctx, r)) return false;
|
||
|
||
Gui_Id widget_id = gui_id_from_pointer(ctx, ratio);
|
||
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;
|
||
*ratio = clamp(0.0f, 1.0f, pointer_ratio);
|
||
}
|
||
|
||
// 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
|
||
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
|
||
gui_button_draw_inner_text(ctx, r, text, text_color);
|
||
|
||
return behaviour || ctx->active == widget_id;
|
||
}
|
||
|
||
bool gui_slider_text(Rect r, f32 *ratio, const char *text)
|
||
{
|
||
return gui_slider_text(&global_gui_state.default_context, r, ratio, text);
|
||
}
|
||
|
||
|
||
// Images
|
||
bool gui_image(Gui_Context *ctx, Rect r, r_texture *texture)
|
||
{
|
||
if(gui_is_clipped(ctx, r)) return false;
|
||
|
||
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)
|
||
{
|
||
if(gui_is_clipped(ctx, r)) return false;
|
||
|
||
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)
|
||
{
|
||
if(gui_is_clipped(ctx, r)) return false;
|
||
|
||
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)
|
||
{
|
||
|
||
// Panels
|
||
// void gui_panel(Gui_Context *ctx, Rect r);
|
||
// void gui_panel(Rect r);
|
||
//
|
||
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;
|
||
}
|
||
|
||
gui_clip_start(ctx, r);
|
||
|
||
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);
|
||
}
|
||
|
||
gui_clip_end(ctx);
|
||
|
||
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);
|
||
}
|
||
|
||
|
||
// Panels
|
||
void gui_panel(Gui_Context *ctx, Rect r)
|
||
{
|
||
Gui_Id widget_id = 0;
|
||
bool behaviuor = gui_button_behaviuor(ctx, widget_id, r);
|
||
|
||
bool is_inactive = true;
|
||
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;
|
||
Rect background_rect = {r.x + 0.5, r.y + 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);
|
||
}
|
||
|
||
void gui_panel(Rect r)
|
||
{
|
||
gui_panel(&global_gui_state.default_context, r);
|
||
}
|
||
|
||
|
||
|
||
// 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.data;
|
||
|
||
bool hovered = gui_is_hovered(ctx, id, r);
|
||
if(hovered && ctx->input.mouse_pressed_this_frame)
|
||
{
|
||
// Bring window on top
|
||
u32 move_count = ctx->windows.count - 1 - window_index;
|
||
if(move_count > 0)
|
||
{
|
||
Gui_Window tmp = *window;
|
||
memmove(ctx->windows.data + window_index, ctx->windows.data + window_index + 1, sizeof(Gui_Window) * move_count);
|
||
|
||
ctx->windows.last() = tmp;
|
||
window_index = ctx->windows.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->windows.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);
|
||
}
|
||
|
||
bool gui_window_with_titlebar_start(Gui_Context *ctx, Rect r, const char *title, Gui_Window_Titlebar_State *state)
|
||
{
|
||
Gui_Id id = gui_id_from_pointer(ctx, title);
|
||
gui_window_start(ctx, r, id);
|
||
|
||
Gui_Titlebar_State *titlebar_state = state ? &state->titlebar : NULL;
|
||
Rect titlebar_r = {0, 0, r.w, ctx->style.font_size};
|
||
bool result = gui_window_titlebar(ctx, titlebar_r, title, titlebar_state);
|
||
|
||
if(state)
|
||
{
|
||
state->inner_r.position = {0, titlebar_r.h};
|
||
state->inner_r.size = r.size - v2{0, titlebar_r.h};
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
bool gui_window_with_titlebar_start(Rect r, const char *title, Gui_Window_Titlebar_State *state)
|
||
{
|
||
return gui_window_with_titlebar_start(&global_gui_state.default_context, r, title, state);
|
||
}
|
||
|
||
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, Gui_Titlebar_State *state)
|
||
{
|
||
Gui_Id widget_id = gui_id_from_pointer(ctx, title);
|
||
bool behaviour = gui_button_behaviuor(ctx, widget_id, r);
|
||
|
||
if(state)
|
||
{
|
||
state->close = false;
|
||
state->move = {0, 0};
|
||
}
|
||
|
||
// Background
|
||
v4 titlebar_color = ctx->style.window_titlebar_color_inactive;
|
||
if(ctx->current_window == &ctx->windows.last())
|
||
{
|
||
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(state)
|
||
state->close = true;
|
||
behaviour = true;
|
||
}
|
||
ctx->style = old_style;
|
||
|
||
// Move
|
||
if(state && ctx->active == widget_id)
|
||
{
|
||
if(ctx->input.mouse_pressed_this_frame)
|
||
state->anchor_point = ctx->input.pointer_position;
|
||
|
||
state->move = ctx->input.pointer_position - state->anchor_point;
|
||
}
|
||
|
||
return behaviour || ctx->active == widget_id;
|
||
}
|
||
|
||
bool gui_window_titlebar(Rect r, const char *title, Gui_Titlebar_State *state)
|
||
{
|
||
return gui_window_titlebar(&global_gui_state.default_context, r, title, state);
|
||
}
|
||
|
||
|
||
bool gui_scrollable_area_start(Gui_Context *ctx, Rect r, v2 area_size, Rect *displayed_r)
|
||
{
|
||
bool behaviour = false;
|
||
Gui_Id widget_id = gui_id_from_pointer(ctx, displayed_r);
|
||
gui_id_stack_push(ctx, widget_id);
|
||
|
||
Rect displayed = {0,0,0,0};
|
||
displayed.size = r.size;
|
||
if(displayed_r)
|
||
displayed.position = displayed_r->position;
|
||
|
||
Rect vertical_scrollbar = {0,0,0,0};
|
||
Rect horizontal_scrollbar = {0,0,0,0};
|
||
if(area_size.y > r.h)
|
||
{
|
||
vertical_scrollbar.w = ctx->style.scrollbar_size;
|
||
vertical_scrollbar.h = r.h;
|
||
vertical_scrollbar.x = r.x + r.w - vertical_scrollbar.w;
|
||
vertical_scrollbar.y = r.y;
|
||
}
|
||
if(area_size.x > r.w)
|
||
{
|
||
horizontal_scrollbar.w = r.w;
|
||
horizontal_scrollbar.h = ctx->style.scrollbar_size;
|
||
horizontal_scrollbar.x = r.x;
|
||
horizontal_scrollbar.y = r.y + r.h - horizontal_scrollbar.h;
|
||
}
|
||
if(vertical_scrollbar.w && horizontal_scrollbar.h)
|
||
{
|
||
vertical_scrollbar.h -= horizontal_scrollbar.h;
|
||
horizontal_scrollbar.w -= vertical_scrollbar.w;
|
||
}
|
||
|
||
displayed.size -= v2{vertical_scrollbar.w, horizontal_scrollbar.h};
|
||
|
||
if(vertical_scrollbar.w)
|
||
{
|
||
f32 relative_y = -(displayed.y - r.y) / area_size.y;
|
||
f32 relative_h = displayed.h / area_size.y;
|
||
Gui_Id vertical_id = gui_id_from_pointer(ctx, &vertical_scrollbar);
|
||
behaviour = behaviour || gui_button_behaviuor(ctx, vertical_id, vertical_scrollbar);
|
||
|
||
if(gui_is_hovered(ctx, widget_id, r))
|
||
{
|
||
behaviour = true;
|
||
f32 scroll_amount = relative_h / 3;
|
||
relative_y += ctx->input.scroll_move * scroll_amount;
|
||
}
|
||
|
||
if(ctx->active == vertical_id)
|
||
{
|
||
behaviour = true;
|
||
f32 relative_pointer = ctx->input.pointer_position.y - vertical_scrollbar.y - 0.5*relative_h*vertical_scrollbar.h;
|
||
relative_pointer /= vertical_scrollbar.h;
|
||
relative_y = relative_pointer;
|
||
}
|
||
|
||
relative_y = clamp(0, 1 - relative_h, relative_y);
|
||
displayed.y = r.y - relative_y * area_size.y;
|
||
|
||
// Render
|
||
r_2d_immediate_rounded_rectangle(vertical_scrollbar, ctx->style.scrollbar_corner_radius, ctx->style.scrollbar_color);
|
||
Rect inner_bar_r = {
|
||
.x = vertical_scrollbar.x + (vertical_scrollbar.w - ctx->style.scrollbar_inner_bar_size) / 2,
|
||
.y = vertical_scrollbar.y + relative_y * vertical_scrollbar.h,
|
||
.w = ctx->style.scrollbar_inner_bar_size,
|
||
.h = relative_h * vertical_scrollbar.h
|
||
};
|
||
r_2d_immediate_rounded_rectangle(inner_bar_r, ctx->style.scrollbar_inner_bar_corner_radius, ctx->style.scrollbar_inner_bar_color);
|
||
}
|
||
if(horizontal_scrollbar.h)
|
||
{
|
||
f32 relative_x = -(displayed.x - r.x) / area_size.x;
|
||
f32 relative_w = displayed.w / area_size.x;
|
||
Gui_Id horizontal_id = gui_id_from_pointer(ctx, &horizontal_scrollbar);
|
||
behaviour = behaviour || gui_button_behaviuor(ctx, horizontal_id, horizontal_scrollbar);
|
||
|
||
if(ctx->active == horizontal_id)
|
||
{
|
||
behaviour = true;
|
||
f32 relative_pointer = ctx->input.pointer_position.x - horizontal_scrollbar.x - 0.5*relative_w*horizontal_scrollbar.w;
|
||
relative_pointer /= horizontal_scrollbar.w;
|
||
relative_x = clamp(0, 1 - relative_w, relative_pointer);
|
||
displayed.x = r.x - relative_x * area_size.x;
|
||
}
|
||
|
||
// Render
|
||
r_2d_immediate_rounded_rectangle(horizontal_scrollbar, ctx->style.scrollbar_corner_radius, ctx->style.scrollbar_color);
|
||
Rect inner_bar_r = {
|
||
.x = horizontal_scrollbar.x + relative_x * horizontal_scrollbar.w,
|
||
.y = horizontal_scrollbar.y + (horizontal_scrollbar.h - ctx->style.scrollbar_inner_bar_size) / 2,
|
||
.w = relative_w * horizontal_scrollbar.w,
|
||
.h = ctx->style.scrollbar_inner_bar_size,
|
||
};
|
||
r_2d_immediate_rounded_rectangle(inner_bar_r, ctx->style.scrollbar_inner_bar_corner_radius, ctx->style.scrollbar_inner_bar_color);
|
||
}
|
||
|
||
if(displayed_r)
|
||
*displayed_r = displayed;
|
||
|
||
gui_clip_start(ctx, Rect{r.x, r.y, displayed.w, displayed.h});
|
||
return behaviour;
|
||
}
|
||
|
||
bool gui_scrollable_area_start(Rect r, v2 area_size, Rect *displayed_r)
|
||
{
|
||
return gui_scrollable_area_start(&global_gui_state.default_context, r, area_size, displayed_r);
|
||
}
|
||
|
||
void gui_scrollable_area_end(Gui_Context *ctx)
|
||
{
|
||
gui_clip_end(ctx);
|
||
gui_id_stack_pop(ctx);
|
||
}
|
||
|
||
void gui_scrollable_area_end()
|
||
{
|
||
gui_scrollable_area_end(&global_gui_state.default_context);
|
||
}
|
||
|
||
|
||
Gui_Window *gui_window_by_id(Gui_Context *ctx, Rect r, Gui_Id id)
|
||
{
|
||
Gui_Window *window = NULL;
|
||
for(u32 i = 0; i < ctx->windows.count; i++)
|
||
{
|
||
if(ctx->windows[i].id == id)
|
||
{
|
||
window = &ctx->windows[i];
|
||
break;
|
||
}
|
||
}
|
||
|
||
if(!window)
|
||
{
|
||
Gui_Window w = {
|
||
.id = id,
|
||
.r = r,
|
||
.framebuffer = r_framebuffer_create(V2S(r.size), 0)
|
||
};
|
||
ctx->windows.push(w);
|
||
window = &ctx->windows.last();
|
||
}
|
||
|
||
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))
|
||
{
|
||
for(u64 i = ctx->clipping.count; i > 0; i--) // Start from the end. The last clipping is usually the smallest and the most likely to fail.
|
||
{
|
||
// @Correctness: I have a feeling this is wrong. What happens with a stack where the first clips where not in a window, while the last ones are in a window? We would have different relative pointer positions to consider. The clipping would also be relative to its parent window/framebuffer.
|
||
if(!is_inside(ctx->clipping[i-1], ctx->input.pointer_position))
|
||
return false;
|
||
}
|
||
|
||
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.data;
|
||
|
||
if(!is_inside(ctx->current_window->r, ctx->input.absolute_pointer_position))
|
||
return false;
|
||
}
|
||
|
||
// 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->windows.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->windows.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 = 0;
|
||
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 = 0;
|
||
// ctx->active_status = 0;
|
||
}
|
||
|
||
return behaviour;
|
||
}
|
||
|
||
Gui_Id gui_id_from_pointer(Gui_Context *ctx, const void* ptr)
|
||
{
|
||
u32 seed = 0xFFFFFFFF;
|
||
if(ctx->id_stack.count)
|
||
seed = ctx->id_stack.last();
|
||
return hash_crc32(&ptr, sizeof(void*), seed);
|
||
}
|
||
|
||
void gui_id_stack_push(Gui_Context *ctx, Gui_Id id)
|
||
{
|
||
ctx->id_stack.push(id);
|
||
}
|
||
|
||
void gui_id_stack_pop(Gui_Context *ctx)
|
||
{
|
||
ctx->id_stack.pop();
|
||
}
|
||
|
||
// Clipping
|
||
static void gui_clip_internal(Gui_Context *ctx, Rect r)
|
||
{
|
||
f32 height = ctx->current_window ? ctx->current_window->r.h : ctx->height;
|
||
glScissor(floor(r.x), floor(height - r.y - r.h), ceil(r.w), ceil(r.h)); // Textures are rendered flipped vertically, so we need to start r.y far away from the bottom and end r.h farther.
|
||
}
|
||
|
||
void gui_clip_start(Gui_Context *ctx, Rect r)
|
||
{
|
||
ctx->clipping.push(r);
|
||
glEnable(GL_SCISSOR_TEST);
|
||
gui_clip_internal(ctx, r);
|
||
}
|
||
|
||
void gui_clip_end(Gui_Context *ctx)
|
||
{
|
||
ctx->clipping.pop();
|
||
if(ctx->clipping.count)
|
||
gui_clip_internal(ctx, ctx->clipping.last());
|
||
else
|
||
glDisable(GL_SCISSOR_TEST);
|
||
}
|
||
|
||
bool gui_is_clipped(Gui_Context *ctx, Rect r)
|
||
{
|
||
for(u64 i = 0; i < ctx->clipping.count; i++)
|
||
{
|
||
// @Correctness: I have a feeling this is wrong. What happens with a stack where the first clips where not in a window, while the last ones are in a window? We would have different relative pointer positions to consider. The clipping would also be relative to its parent window/framebuffer.
|
||
if(!is_inside(ctx->clipping[i], r.position) && !is_inside(ctx->clipping[i], r.position + r.size))
|
||
return true;
|
||
}
|
||
if(ctx->current_window)
|
||
{
|
||
Rect window_r = {0, 0, ctx->current_window->r.w, ctx->current_window->r.h};
|
||
if(!is_inside(window_r, r.position) && !is_inside(window_r, r.position + r.size))
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|