A 3D game engine from scratch.
at main 665 lines 21 kB view raw
1// (c) 2020 Vlad-Stefan Harbuz <vlad@vladh.net> 2 3#include "../src_external/glad/glad.h" 4#include <GLFW/glfw3.h> 5#include "gui.hpp" 6#include "util.hpp" 7#include "logs.hpp" 8#include "intrinsics.hpp" 9 10 11gui::State *gui::state = nullptr; 12 13 14void 15gui::update_screen_dimensions(u32 new_window_width, u32 new_window_height) 16{ 17 gui::state->window_dimensions = v2(new_window_width, new_window_height); 18} 19 20 21void 22gui::update_mouse_button() 23{ 24 if (input::is_mouse_button_now_up(GLFW_MOUSE_BUTTON_LEFT)) { 25 gui::state->container_being_moved = nullptr; 26 } 27} 28 29 30void 31gui::update_mouse() 32{ 33 if (gui::state->container_being_moved) { 34 gui::state->container_being_moved->position += input::get_mouse_offset(); 35 } 36} 37 38 39void 40gui::update() 41{ 42 set_cursor(); 43} 44 45 46gui::Container * 47gui::make_container(const char *title, v2 position) 48{ 49 Container *container = nullptr; 50 each (container_candidate, gui::state->containers) { 51 if (strcmp(container_candidate->title, title) == 0) { 52 container = container_candidate; 53 break; 54 } 55 } 56 57 if (container) { 58 // Check if we need to set this container as being moved. 59 if ( 60 input::is_mouse_in_bb(container->position, 61 container->position + v2(container->dimensions.x, container->title_bar_height)) && 62 input::is_mouse_button_now_down(GLFW_MOUSE_BUTTON_LEFT) 63 ) { 64 gui::state->container_being_moved = container; 65 } 66 67 // Draw the container with the information from the previous frame 68 // if there is anything in it. 69 if (container->content_dimensions != v2(0.0f, 0.0f)) { 70 draw_container(container); 71 } 72 } else { 73 container = gui::state->containers.push(); 74 container->title = title; 75 container->position = position; 76 container->direction = v2(0.0f, 1.0f); 77 container->padding = v2(20.0f); 78 container->title_bar_height = 40.0f; 79 container->element_margin = 20.0f; 80 } 81 82 // In all cases, clear this container. 83 container->n_elements = 0; 84 container->dimensions = container->padding * 2.0f; 85 container->content_dimensions = v2(0.0f, 0.0f); 86 container->next_element_position = container->position + 87 container->padding + 88 v2(0.0f, container->title_bar_height); 89 90 return container; 91} 92 93 94void 95gui::draw_heading(const char *str, v4 color) 96{ 97 auto bb = center_bb(v2(0.0f, 0.0f), 98 gui::state->window_dimensions, 99 get_text_dimensions(fonts::get_by_name(gui::state->font_assets, "heading"), str)); 100 v2 position = v2(bb.x, 90.0f); 101 draw_text_shadow("heading", str, position, color); 102 draw_text("heading", str, position, color); 103} 104 105 106void 107gui::tick_heading() 108{ 109 if (gui::state->heading_opacity > 0.0f) { 110 draw_heading(gui::state->heading_text, 111 v4(0.0f, 0.33f, 0.93f, gui::state->heading_opacity)); 112 if (gui::state->heading_fadeout_delay > 0.0f) { 113 gui::state->heading_fadeout_delay -= (f32)(engine::get_dt()); 114 } else { 115 gui::state->heading_opacity -= 116 gui::state->heading_fadeout_duration * (f32)(engine::get_dt()); 117 } 118 } 119} 120 121 122bool 123gui::draw_toggle(Container *container, const char *text, bool toggle_state) 124{ 125 bool is_pressed = false; 126 127 v2 text_dimensions = get_text_dimensions(fonts::get_by_name(gui::state->font_assets, "body"), text); 128 v2 button_dimensions = TOGGLE_BUTTON_SIZE + BUTTON_DEFAULT_BORDER * 2.0f; 129 v2 dimensions = v2(button_dimensions.x + TOGGLE_SPACING + text_dimensions.x, 130 max(button_dimensions.y, text_dimensions.y)); 131 132 v2 position = add_element_to_container(container, dimensions); 133 134 v2 button_bottomright = position + button_dimensions; 135 v2 text_centered_position = center_bb(position, button_dimensions, text_dimensions); 136 v2 text_position = v2(position.x + button_dimensions.x + TOGGLE_SPACING, 137 text_centered_position.y); 138 139 v4 button_color; 140 if (toggle_state) { 141 button_color = MAIN_COLOR; 142 } else { 143 button_color = LIGHT_COLOR; 144 } 145 146 if (input::is_mouse_in_bb(position, button_bottomright)) { 147 request_cursor(input::CursorType::hand); 148 if (toggle_state) { 149 button_color = MAIN_HOVER_COLOR; 150 } else { 151 button_color = LIGHT_HOVER_COLOR; 152 } 153 154 if (input::is_mouse_button_now_down(GLFW_MOUSE_BUTTON_LEFT)) { 155 is_pressed = true; 156 } 157 158 if (input::is_mouse_button_down(GLFW_MOUSE_BUTTON_LEFT)) { 159 if (toggle_state) { 160 button_color = MAIN_ACTIVE_COLOR; 161 } else { 162 button_color = LIGHT_ACTIVE_COLOR; 163 } 164 } 165 } 166 167 draw_frame(position, button_bottomright, TOGGLE_BUTTON_DEFAULT_BORDER, LIGHT_DARKEN_COLOR); 168 draw_rect(position + TOGGLE_BUTTON_DEFAULT_BORDER, 169 button_dimensions - (TOGGLE_BUTTON_DEFAULT_BORDER * 2.0f), button_color); 170 draw_text("body", text, text_position, LIGHT_TEXT_COLOR); 171 172 return is_pressed; 173} 174 175 176void 177gui::draw_named_value(Container *container, const char *name_text, const char *value_text) 178{ 179 v2 name_text_dimensions = get_text_dimensions(fonts::get_by_name(gui::state->font_assets, "body-bold"), 180 name_text); 181 v2 value_text_dimensions = get_text_dimensions(fonts::get_by_name(gui::state->font_assets, "body"), 182 value_text); 183 // Sometimes we draw a value which is a rapidly changing number. 184 // We don't want to container to wobble in size back and forth, so we round 185 // the size of the value text to the next multiple of 50. 186 value_text_dimensions.x = util::round_to_nearest_multiple(value_text_dimensions.x, 50.0f); 187 v2 dimensions = v2(value_text_dimensions.x + NAMED_VALUE_NAME_WIDTH, 188 max(name_text_dimensions.y, value_text_dimensions.y)); 189 190 v2 position = add_element_to_container(container, dimensions); 191 192 draw_text("body-bold", name_text, position, LIGHT_TEXT_COLOR); 193 194 v2 value_text_position = position + v2(NAMED_VALUE_NAME_WIDTH, 0.0f); 195 draw_text("body", value_text, value_text_position, LIGHT_TEXT_COLOR); 196} 197 198 199void 200gui::draw_body_text(Container *container, const char *text) 201{ 202 v2 dimensions = get_text_dimensions(fonts::get_by_name(gui::state->font_assets, "body"), 203 text); 204 v2 position = add_element_to_container(container, dimensions); 205 draw_text("body", text, position, LIGHT_TEXT_COLOR); 206} 207 208 209bool 210gui::draw_button(Container *container, const char *text) 211{ 212 bool is_pressed = false; 213 214 v2 text_dimensions = get_text_dimensions(fonts::get_by_name(gui::state->font_assets, "body"), text); 215 v2 button_dimensions = text_dimensions + BUTTON_AUTOSIZE_PADDING + BUTTON_DEFAULT_BORDER * 2.0f; 216 217 v2 position = add_element_to_container(container, button_dimensions); 218 219 v2 bottomright = position + button_dimensions; 220 v2 text_position = center_bb(position, button_dimensions, text_dimensions); 221 222 v4 button_color = MAIN_COLOR; 223 224 if (input::is_mouse_in_bb(position, bottomright)) { 225 request_cursor(input::CursorType::hand); 226 button_color = MAIN_HOVER_COLOR; 227 228 if (input::is_mouse_button_now_down(GLFW_MOUSE_BUTTON_LEFT)) { 229 is_pressed = true; 230 } 231 232 if (input::is_mouse_button_down(GLFW_MOUSE_BUTTON_LEFT)) { 233 button_color = MAIN_ACTIVE_COLOR; 234 } 235 } 236 237 draw_frame(position, bottomright, BUTTON_DEFAULT_BORDER, MAIN_DARKEN_COLOR); 238 draw_rect(position + BUTTON_DEFAULT_BORDER, 239 button_dimensions - (BUTTON_DEFAULT_BORDER * 2.0f), button_color); 240 draw_text("body", text, text_position, LIGHT_TEXT_COLOR); 241 242 return is_pressed; 243} 244 245 246void 247gui::draw_console(char *console_input_text) 248{ 249 if (!gui::state->console.is_enabled) { 250 return; 251 } 252 253 fonts::FontAsset *font_asset = fonts::get_by_name(gui::state->font_assets, "body"); 254 f32 line_height = fonts::font_unit_to_px(font_asset->height); 255 f32 line_spacing = floor(line_height * CONSOLE_LINE_SPACING_FACTOR); 256 257 // Draw console log 258 { 259 v2 next_element_position = v2(CONSOLE_PADDING.x, MAX_CONSOLE_LOG_HEIGHT); 260 261 draw_rect(v2(0.0f, 0.0f), 262 v2(gui::state->window_dimensions.x, MAX_CONSOLE_LOG_HEIGHT), 263 CONSOLE_BG_COLOR); 264 265 u32 idx_line = gui::state->console.idx_log_start; 266 while (idx_line != gui::state->console.idx_log_end) { 267 v2 text_dimensions = get_text_dimensions(font_asset, gui::state->console.log[idx_line]); 268 next_element_position.y -= text_dimensions.y + line_spacing; 269 draw_text("body", gui::state->console.log[idx_line], next_element_position, LIGHT_TEXT_COLOR); 270 271 idx_line++; 272 if (idx_line == MAX_N_CONSOLE_LINES) { 273 idx_line = 0; 274 } 275 } 276 } 277 278 // Draw console input 279 { 280 f32 console_input_height = line_height + (2.0f * CONSOLE_PADDING.y); 281 v2 console_input_position = v2(0.0f, MAX_CONSOLE_LOG_HEIGHT); 282 283 draw_rect(console_input_position, 284 v2(gui::state->window_dimensions.x, console_input_height), 285 MAIN_DARKEN_COLOR); 286 287 draw_text("body", console_input_text, 288 console_input_position + CONSOLE_PADDING, LIGHT_TEXT_COLOR); 289 } 290} 291 292 293bool 294gui::is_console_enabled() 295{ 296 return gui::state->console.is_enabled; 297} 298 299 300void 301gui::set_console_enabled(bool val) 302{ 303 gui::state->console.is_enabled = val; 304} 305 306 307void 308gui::log(const char *format, ...) 309{ 310 char text[MAX_CONSOLE_LINE_LENGTH]; 311 va_list vargs; 312 va_start(vargs, format); 313 vsnprintf(text, sizeof(text), format, vargs); 314 va_end(vargs); 315 316 // Fill array in back-to-front. 317 if (gui::state->console.idx_log_start == 0) { 318 gui::state->console.idx_log_start = MAX_N_CONSOLE_LINES - 1; 319 } else { 320 gui::state->console.idx_log_start--; 321 } 322 if (gui::state->console.idx_log_start == gui::state->console.idx_log_end) { 323 if (gui::state->console.idx_log_end == 0) { 324 gui::state->console.idx_log_end = MAX_N_CONSOLE_LINES - 1; 325 } else { 326 gui::state->console.idx_log_end--; 327 } 328 } 329 strcpy(gui::state->console.log[gui::state->console.idx_log_start], text); 330} 331 332 333void 334gui::set_heading( 335 const char *text, f32 opacity, 336 f32 fadeout_duration, f32 fadeout_delay 337) { 338 gui::state->heading_text = text; 339 gui::state->heading_opacity = opacity; 340 gui::state->heading_fadeout_duration = fadeout_duration; 341 gui::state->heading_fadeout_delay = fadeout_delay; 342} 343 344 345void 346gui::init( 347 memory::Pool *memory_pool, 348 gui::State* gui_state, 349 iv2 texture_atlas_size, 350 Array<fonts::FontAsset> *font_assets, 351 u32 window_width, u32 window_height 352) { 353 gui::state = gui_state; 354 355 gui::state->containers = Array<Container>(memory_pool, 32, "gui_containers"); 356 gui::state->texture_atlas_size = texture_atlas_size; 357 gui::state->font_assets = font_assets; 358 gui::state->window_dimensions = v2(window_width, window_height); 359 log("Hello world!"); 360} 361 362 363void 364gui::request_cursor(input::CursorType cursor) 365{ 366 gui::state->requested_cursor = cursor; 367} 368 369 370void 371gui::set_cursor() 372{ 373 input::set_cursor(gui::state->requested_cursor); 374 gui::state->requested_cursor = input::CursorType::none; 375} 376 377 378void 379gui::push_vertices(f32 *vertices, u32 n_vertices) 380{ 381 renderer::push_gui_vertices(vertices, n_vertices); 382} 383 384 385v2 386gui::get_text_dimensions(fonts::FontAsset *font_asset, char const *str) 387{ 388 // NOTE: This returns the dimensions around the main body of the text. 389 // This does not include descenders. 390 f32 line_height = fonts::font_unit_to_px(font_asset->height); 391 f32 line_spacing = line_height * LINE_SPACING_FACTOR; 392 f32 ascender = fonts::font_unit_to_px(font_asset->ascender); 393 394 f32 start_x = 0.0f; 395 f32 start_y = 0.0f - (line_height - ascender); 396 f32 max_x = 0.0f; 397 f32 curr_x = start_x; 398 f32 curr_y = start_y; 399 size_t str_length = strlen(str); 400 401 for (u32 idx = 0; idx < str_length; idx++) { 402 char c = str[idx]; 403 404 if (c < 32) { 405 if (c == '\n') { 406 max_x = max(max_x, curr_x); 407 curr_x = 0.0f; 408 curr_y += line_spacing; 409 } 410 continue; 411 } 412 413 fonts::Character *character = font_asset->characters[c]; 414 415 if (!character) { 416 logs::warning("Could not get character: %c", c); 417 continue; 418 } 419 420 curr_x += fonts::frac_px_to_px(character->advance.x); 421 curr_y += fonts::frac_px_to_px(character->advance.y); 422 } 423 424 max_x = max(max_x, curr_x); 425 curr_y += line_height; 426 427 return v2(max_x, curr_y); 428} 429 430 431v2 432gui::center_bb(v2 container_position, v2 container_dimensions, v2 element_dimensions) 433{ 434 return ceil(container_position + (container_dimensions / 2.0f) - (element_dimensions / 2.0f)); 435} 436 437 438v2 439gui::add_element_to_container(Container *container, v2 element_dimensions) 440{ 441 // When adding a new element, we need to ensure we have enough space. 442 // 443 // We need: 444 // * Enough space for the element itself. 445 // * If this is not the first element, enough space for one 446 // `element_margin` on the main axis. 447 // 448 // On the main axis, we will allocate new space of this size. 449 // On the orthogonal axis, we will ensure the element's dimensions are at 450 // least this big, taking the container padding into account/ 451 // 452 // For example, if we add a button that is 200x20 to a container which already 453 // has buttons, we will add (20 + element_margin) to its height, and ensure 454 // its width is at least (200 + padding). 455 456 v2 new_element_position = container->next_element_position; 457 v2 orthogonal_direction = v2(container->direction.y, container->direction.x); 458 459 v2 required_space = element_dimensions; 460 if (container->n_elements > 0) { 461 required_space += (container->element_margin * container->direction); 462 } 463 464 auto d0 = (container->content_dimensions + required_space) * container->direction; 465 auto d1 = max(container->content_dimensions, required_space) * orthogonal_direction; 466 container->content_dimensions = d0 + d1; 467 container->dimensions = container->content_dimensions + 468 (container->padding * 2.0f) + 469 v2(0.0f, container->title_bar_height); 470 471 d0 = (container->dimensions - container->padding + container->element_margin) * container->direction; 472 d1 = (container->padding + v2(0.0f, container->title_bar_height)) * orthogonal_direction; 473 container->next_element_position = container->position + d0 + d1; 474 475 container->n_elements++; 476 477 return new_element_position; 478} 479 480 481void 482gui::draw_rect(v2 position, v2 dimensions, v4 color) 483{ 484 // NOTE: We use top-left as our origin, but OpenGL uses bottom-left. 485 // Flip the y axis before drawing. 486 f32 x0 = position.x; 487 f32 x1 = x0 + dimensions.x; 488 f32 y0 = (f32)gui::state->window_dimensions.y - position.y; 489 f32 y1 = y0 - dimensions.y; 490 491 f32 vertices[VERTEX_LENGTH * 6] = { 492 x0, y0, -1.0f, -1.0f, color.r, color.g, color.b, color.a, 493 x0, y1, -1.0f, -1.0f, color.r, color.g, color.b, color.a, 494 x1, y1, -1.0f, -1.0f, color.r, color.g, color.b, color.a, 495 x0, y0, -1.0f, -1.0f, color.r, color.g, color.b, color.a, 496 x1, y1, -1.0f, -1.0f, color.r, color.g, color.b, color.a, 497 x1, y0, -1.0f, -1.0f, color.r, color.g, color.b, color.a 498 }; 499 push_vertices(vertices, 6); 500} 501 502 503void 504gui::draw_text( 505 char const *font_name, char const *str, 506 v2 position, 507 v4 color 508) { 509 fonts::FontAsset *font_asset = fonts::get_by_name(gui::state->font_assets, font_name); 510 511 f32 line_height = fonts::font_unit_to_px(font_asset->height); 512 f32 line_spacing = line_height * LINE_SPACING_FACTOR; 513 f32 ascender = fonts::font_unit_to_px(font_asset->ascender); 514 515 // NOTE: When changing this code, remember that the text positioning logic 516 // needs to be replicated in `get_text_dimensions()`! 517 f32 start_x = position.x; 518 f32 start_y = position.y - (line_height - ascender); 519 f32 curr_x = start_x; 520 f32 curr_y = start_y; 521 size_t str_length = strlen(str); 522 523 for (u32 idx = 0; idx < str_length; idx++) { 524 char c = str[idx]; 525 526 if (c < 32) { 527 if (c == '\n') { 528 curr_x = start_x; 529 curr_y += line_spacing; 530 } 531 continue; 532 } 533 534 fonts::Character *character = font_asset->characters[c]; 535 536 if (!character) { 537 logs::warning("Could not get character: %c", c); 538 continue; 539 } 540 541 f32 char_x = curr_x + character->bearing.x; 542 f32 char_y = curr_y + fonts::font_unit_to_px(font_asset->height) - character->bearing.y; 543 544 f32 tex_x = (f32)character->tex_coords.x / gui::state->texture_atlas_size.x; 545 f32 tex_y = (f32)character->tex_coords.y / gui::state->texture_atlas_size.y; 546 f32 tex_w = (f32)character->size.x / gui::state->texture_atlas_size.x; 547 f32 tex_h = (f32)character->size.y / gui::state->texture_atlas_size.y; 548 549 f32 w = (f32)character->size.x; 550 f32 h = (f32)character->size.y; 551 552 curr_x += fonts::frac_px_to_px(character->advance.x); 553 curr_y += fonts::frac_px_to_px(character->advance.y); 554 555 // Skip glyphs with no pixels, like spaces. 556 if (w <= 0 || h <= 0) { 557 continue; 558 } 559 560 // NOTE: We use top-left as our origin, but OpenGL uses bottom-left. 561 // Flip the y axis before drawing. 562 f32 x0 = char_x; 563 f32 x1 = x0 + w; 564 f32 y0 = (f32)gui::state->window_dimensions.y - char_y; 565 f32 y1 = y0 - h; 566 567 f32 tex_x0 = tex_x; 568 f32 tex_x1 = tex_x0 + tex_w; 569 f32 tex_y0 = tex_y; 570 f32 tex_y1 = tex_y0 + tex_h; 571 572 f32 vertices[VERTEX_LENGTH * 6] = { 573 x0, y0, tex_x0, tex_y0, color.r, color.g, color.b, color.a, 574 x0, y1, tex_x0, tex_y1, color.r, color.g, color.b, color.a, 575 x1, y1, tex_x1, tex_y1, color.r, color.g, color.b, color.a, 576 x0, y0, tex_x0, tex_y0, color.r, color.g, color.b, color.a, 577 x1, y1, tex_x1, tex_y1, color.r, color.g, color.b, color.a, 578 x1, y0, tex_x1, tex_y0, color.r, color.g, color.b, color.a 579 }; 580 push_vertices(vertices, 6); 581 } 582} 583 584 585void 586gui::draw_text_shadow( 587 char const *font_name, char const *str, 588 v2 position, 589 v4 color 590) { 591 draw_text(font_name, str, position + TEXT_SHADOW_OFFSET, 592 v4(0.0f, 0.0f, 0.0f, color.a * 0.2f)); 593} 594 595 596void 597gui::draw_container(Container *container) 598{ 599 draw_rect(container->position, 600 v2(container->dimensions.x, container->title_bar_height), 601 MAIN_DARKEN_COLOR); 602 603 v2 text_dimensions = get_text_dimensions(fonts::get_by_name(gui::state->font_assets, "body"), 604 container->title); 605 v2 centered_text_position = center_bb(container->position, 606 v2(container->dimensions.x, container->title_bar_height), 607 text_dimensions); 608 v2 text_position = v2(container->position.x + container->padding.x, centered_text_position.y); 609 draw_text_shadow("body", container->title, text_position, LIGHT_TEXT_COLOR); 610 draw_text("body", container->title, text_position, LIGHT_TEXT_COLOR); 611 612 draw_rect(container->position + v2(0.0, container->title_bar_height), 613 container->dimensions - v2(0.0, container->title_bar_height), 614 WINDOW_BG_COLOR); 615} 616 617 618void 619gui::draw_line(v2 start, v2 end, f32 thickness, v4 color) 620{ 621 // NOTE: We use top-left as our origin, but OpenGL uses bottom-left. 622 // Flip the y axis before drawing. 623 v2 delta = normalize(end - start) * thickness; 624 625 // -----------> 626 // 0------------------3 627 // | | 628 // 1------------------2 629 f32 x0 = start.x + delta.y; 630 f32 y0 = gui::state->window_dimensions.y - start.y; 631 f32 x1 = start.x; 632 f32 y1 = gui::state->window_dimensions.y - start.y - delta.x; 633 f32 x2 = end.x; 634 f32 y2 = gui::state->window_dimensions.y - end.y - delta.x; 635 f32 x3 = end.x + delta.y; 636 f32 y3 = gui::state->window_dimensions.y - end.y; 637 638 f32 vertices[VERTEX_LENGTH * 6] = { 639 x0, y0, -1.0f, -1.0f, color.r, color.g, color.b, color.a, 640 x1, y1, -1.0f, -1.0f, color.r, color.g, color.b, color.a, 641 x2, y2, -1.0f, -1.0f, color.r, color.g, color.b, color.a, 642 x0, y0, -1.0f, -1.0f, color.r, color.g, color.b, color.a, 643 x2, y2, -1.0f, -1.0f, color.r, color.g, color.b, color.a, 644 x3, y3, -1.0f, -1.0f, color.r, color.g, color.b, color.a 645 }; 646 push_vertices(vertices, 6); 647} 648 649 650void 651gui::draw_frame(v2 position, v2 bottomright, v2 thickness, v4 color) 652{ 653 draw_line(v2(position.x, position.y), 654 v2(bottomright.x, position.y), 655 thickness.y, color); 656 draw_line(v2(position.x, bottomright.y - thickness.y), 657 v2(bottomright.x, bottomright.y - thickness.y), 658 thickness.y, color); 659 draw_line(v2(position.x, position.y), 660 v2(position.x, bottomright.y), 661 thickness.x, color); 662 draw_line(v2(bottomright.x - thickness.x, position.y), 663 v2(bottomright.x - thickness.x, bottomright.y), 664 thickness.x, color); 665}