A 3D game engine from scratch.
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}