Files
Server_Monitor/code/gui/gui.cpp
2024-09-27 19:46:39 +02:00

1073 lines
32 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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;
}