Files
cudoku/ui.c
T

580 lines
17 KiB
C
Raw Normal View History

#include <stdio.h>
#include <math.h>
#include <ft2build.h>
#include FT_FREETYPE_H
#include <freetype/ftmm.h>
#include <X11/Xlib.h>
#include <glad/glx.h>
#include "cudoku.h"
#include "shader.h"
#include "audio.h"
#include "ui.h"
#include "x11.h"
2023-11-21 21:39:03 +02:00
#define FONT_PIXEL_SIZE 128
Display *display;
Window window;
Context zephr_context;
Character characters[128];
Shader font_shader;
Shader ui_shader;
unsigned int font_vao;
unsigned int ui_vao;
unsigned int font_vbo;
unsigned int ui_vbo;
// TODO: instanced rendering of ui elements and text??
int init_fonts(const char *font_path) {
FT_Library ft;
if (FT_Init_FreeType(&ft)) {
return -1;
}
FT_Face face;
if (FT_New_Face(ft, font_path, 0, &face)) {
return -2;
}
if ((face->face_flags & FT_FACE_FLAG_MULTIPLE_MASTERS)) {
printf("[INFO] Got a variable font\n");
FT_MM_Var *mm;
FT_Get_MM_Var(face, &mm);
FT_Set_Var_Design_Coordinates(face, mm->num_namedstyles, mm->namedstyle[mm->num_namedstyles - 4].coords);
FT_Done_MM_Var(ft, mm);
}
2023-11-21 21:39:03 +02:00
FT_Set_Pixel_Sizes(face, 0, FONT_PIXEL_SIZE);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
for (int c = 0; c < 128; c++) {
if (FT_Load_Char(face, c, FT_LOAD_RENDER)) {
printf("[ERROR]: failed to load glyph for char '%c'\n", c);
}
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, face->glyph->bitmap.width,
face->glyph->bitmap.rows, 0, GL_RED, GL_UNSIGNED_BYTE,
face->glyph->bitmap.buffer);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
characters[c].texture_id = texture;
characters[c].advance = face->glyph->advance.x;
characters[c].size = (Size){ .width = face->glyph->bitmap.width, .height = face->glyph->bitmap.rows };
characters[c].bearing = (Size){ .width = face->glyph->bitmap_left, .height = face->glyph->bitmap_top };
}
glBindTexture(GL_TEXTURE_2D, 0);
FT_Done_Face(face);
FT_Done_FreeType(ft);
return 0;
}
int init_ui(const char* font_path, Size window_size) {
zephr_context.window_size = window_size;
zephr_context.projection = orthographic_projection_2d(0.f, window_size.width, window_size.height, 0.f);
int res = init_fonts(font_path);
if (res == -1) {
printf("[ERROR]: could not initialize freetype library\n");
return 1;
} else if (res == -2) {
printf("[ERROR]: could not load font file: \"%s\"\n", font_path);
return 1;
}
2023-11-21 06:45:54 +02:00
font_shader = create_shader("shaders/ui.vert", "shaders/font.frag");
ui_shader = create_shader("shaders/ui.vert", "shaders/ui.frag");
glGenVertexArrays(1, &font_vao);
glGenVertexArrays(1, &ui_vao);
glGenBuffers(1, &font_vbo);
glGenBuffers(1, &ui_vbo);
glBindVertexArray(font_vao);
glBindBuffer(GL_ARRAY_BUFFER, font_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, NULL, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)0);
glBindVertexArray(ui_vao);
glBindBuffer(GL_ARRAY_BUFFER, ui_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, NULL, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
return 0;
}
int init_zephr(const char* font_path, const char* window_title, Size window_size, Color *clear_color) {
int res = audio_init();
if (res != 0) {
printf("[ERROR]: failed to initialize audio\n");
return 1;
}
res = init_x11(&display, &window, window_title, window_size.width, window_size.height);
if (res < 0) return 1;
res = init_ui(font_path, (Size){ .width = window_size.width, .height = window_size.height });
if (res != 0) {
printf("[ERROR]: failed to initialize ui\n");
return 1;
}
if (clear_color) {
zephr_context.clear_color = *clear_color;
} else {
zephr_context.clear_color = (Color){0.0, 0.0, 0.0, 1.0};
}
start_internal_timer();
return 0;
}
void deinit_zephr() {
close_x11(&display, &window);
audio_close();
}
bool zephr_should_quit() {
// TODO: input events
// TODO: window events
// TODO: ui update??
while (XPending(display)) {
XEvent xev;
XNextEvent(display, &xev);
if (xev.type == ConfigureNotify) {
XConfigureEvent xce = xev.xconfigure;
if (xce.width != zephr_context.window_size.width || xce.height != zephr_context.window_size.height) {
zephr_context.window_size = (Size){ .width = xce.width, .height = xce.height };
zephr_context.projection = orthographic_projection_2d(0.f, xce.width, xce.height, 0.f);
resize_x11_window(display, window);
/* set_scale_factor(xce.width, xce.height, &x_scale, &y_scale); */
}
} else if (xev.type == KeyPress) {
if (XLookupKeysym(&xev.xkey, 0) == XK_Escape ||
(XLookupKeysym(&xev.xkey, 0) == XK_q && xev.xkey.state & ControlMask)) {
zephr_context.should_quit = true;
break;
} else {
/* handle_keypress(xev, &game); */
}
} else if (xev.type == ButtonPress) {
/* if (xev.xbutton.button == Button1) { */
/* int x = xev.xbutton.x; */
/* int y = xev.xbutton.y; */
/* do_selection(&game, x, y, width, height, x_scale, y_scale); */
/* } */
}
}
float r = zephr_context.clear_color.r;
float g = zephr_context.clear_color.g;
float b = zephr_context.clear_color.b;
float a = zephr_context.clear_color.a;
glClearColor(r, g, b, a);
glClear(GL_COLOR_BUFFER_BIT);
/* update_ui(); */
audio_update();
return zephr_context.should_quit;
}
void zephr_swap_buffers() {
glXSwapBuffers(display, window);
}
Size zephr_get_window_size() {
return zephr_context.window_size;
}
Sizef calculate_text_size(const char *text, int font_size) {
2023-11-21 21:39:03 +02:00
float scale = (float)font_size / FONT_PIXEL_SIZE;
Sizef size = { .width = 0, .height = 0 };
int w = 0;
int h = 0;
int max_bearing_h = 0;
// NOTE: I don't like looping through the characters twice, but it's fine for now
for (uint i = 0; i < strlen(text); i++) {
Character ch = characters[(uint)text[i]];
max_bearing_h = max(max_bearing_h, ch.bearing.height);
}
for (uint i = 0; i < strlen(text); i++) {
Character ch = characters[(uint)text[i]];
w += ch.advance / 64;
// remove bearing of first character
if (i == 0 && strlen(text) > 1) {
w -= ch.bearing.width;
}
// remove the trailing width of the last character
if (i == strlen(text) - 1) {
w -= ((ch.advance / 64) - (ch.bearing.width + ch.size.width));
}
// if we only have one character in the text, then remove the bearing width
if (strlen(text) == 1) {
w -= (ch.bearing.width);
}
h = max(h, max_bearing_h - ch.bearing.height + ch.size.height);
}
size.width = (float)w * scale;
size.height = (float)h * scale;
return size;
}
void set_x_constraint(UIConstraints *constraints, float value, UIConstraint type) {
switch (type) {
case UI_CONSTRAINT_RELATIVE_PIXELS:
case UI_CONSTRAINT_ASPECT_RATIO:
// no-op. fallback to FIXED
case UI_CONSTRAINT_FIXED:
constraints->x = value;
break;
case UI_CONSTRAINT_RELATIVE:
constraints->x = (value * zephr_context.window_size.width);
break;
}
}
void set_y_constraint(UIConstraints *constraints, float value, UIConstraint type) {
switch (type) {
case UI_CONSTRAINT_RELATIVE_PIXELS:
case UI_CONSTRAINT_ASPECT_RATIO:
// no-op. fallback to FIXED
case UI_CONSTRAINT_FIXED:
constraints->y = value;
break;
case UI_CONSTRAINT_RELATIVE:
constraints->y = (value * zephr_context.window_size.height);
break;
}
}
void set_width_constraint(UIConstraints *constraints, float value, UIConstraint type) {
switch (type) {
case UI_CONSTRAINT_FIXED:
constraints->width = value;
break;
case UI_CONSTRAINT_RELATIVE:
constraints->width = (value * zephr_context.window_size.width);
break;
case UI_CONSTRAINT_RELATIVE_PIXELS:
constraints->width = zephr_context.window_size.width / 1920.f * value;
break;
case UI_CONSTRAINT_ASPECT_RATIO:
constraints->width = constraints->height * value;
break;
}
}
void set_height_constraint(UIConstraints *constraints, float value, UIConstraint type) {
switch (type) {
case UI_CONSTRAINT_FIXED:
constraints->height = value;
break;
case UI_CONSTRAINT_RELATIVE:
constraints->height = (value * zephr_context.window_size.height);
break;
case UI_CONSTRAINT_RELATIVE_PIXELS:
constraints->height = zephr_context.window_size.height / 1080.f * value;
break;
case UI_CONSTRAINT_ASPECT_RATIO:
constraints->height = constraints->width * value;
break;
}
}
void set_rotation_constraint(UIConstraints *constraints, float angle_d) {
constraints->rotation = angle_d;
}
void apply_constraints(UIConstraints constraints, Vec2f *pos, Sizef *size) {
*pos = (Vec2f){constraints.x, constraints.y};
*size = (Sizef){constraints.width, constraints.height};
}
void apply_alignment(Alignment align, Vec2f *pos, Sizef size) {
switch (align) {
case ALIGN_TOP_LEFT:
pos->x = pos->x;
pos->y = pos->y;
break;
case ALIGN_TOP_CENTER:
pos->x += zephr_context.window_size.width / 2.f - size.width / 2.f;
pos->y = pos->y;
break;
case ALIGN_TOP_RIGHT:
pos->x += zephr_context.window_size.width - size.width;
pos->y = pos->y;
break;
case ALIGN_BOTTOM_LEFT:
pos->x = pos->x;
pos->y += zephr_context.window_size.height - size.height;
break;
case ALIGN_BOTTOM_CENTER:
pos->x += zephr_context.window_size.width / 2.f - size.width / 2.f;
pos->y += zephr_context.window_size.height - size.height;
break;
case ALIGN_BOTTOM_RIGHT:
pos->x += zephr_context.window_size.width - size.width;
pos->y += zephr_context.window_size.height - size.height;
break;
case ALIGN_LEFT_CENTER:
pos->x = pos->x;
pos->y += zephr_context.window_size.height / 2.f - size.height / 2.f;
break;
case ALIGN_RIGHT_CENTER:
pos->x += zephr_context.window_size.width - size.width;
pos->y += zephr_context.window_size.height / 2.f - size.height / 2.f;
break;
case ALIGN_CENTER:
pos->x += zephr_context.window_size.width / 2.f - size.width / 2.f;
pos->y += zephr_context.window_size.height / 2.f - size.height / 2.f;
break;
}
}
2023-11-21 06:45:54 +02:00
void draw_quad(UIConstraints constraints, Color *color, float border_radius, Alignment align) {
use_shader(ui_shader);
if (color) {
set_vec4f(ui_shader, "aColor", color->r / 255.f, color->g / 255.f, color->b / 255.f, color->a / 255.f);
} else {
set_vec4f(ui_shader, "aColor", 0.f, 0.f, 0.f, 1.f);
}
2023-11-21 06:45:54 +02:00
set_float(ui_shader, "uiWidth", constraints.width);
set_float(ui_shader, "uiHeight", constraints.height);
set_float(ui_shader, "borderRadius", border_radius);
set_mat4f(ui_shader, "projection", (float *)zephr_context.projection.m);
Vec2f pos = { 0.f, 0.f };
Sizef size = { 0.f, 0.f };
apply_constraints(constraints, &pos, &size);
apply_alignment(align, &pos, size);
2023-11-21 06:45:54 +02:00
Matrix4x4 model = identity();
apply_translation(&model, (Vec2f){-size.width / 2.f, -size.height / 2.f});
apply_rotation(&model, to_radians(constraints.rotation));
apply_translation(&model, (Vec2f){size.width / 2.f, size.height / 2.f});
apply_translation(&model, pos);
2023-11-21 06:45:54 +02:00
set_mat4f(font_shader, "model", (float *)model.m);
glBindVertexArray(ui_vao);
float vertices[6][4] = {
// bottom left tri
{0, 0 + size.height, 0.0, 1.0},
{0, 0, 0.0, 0.0},
{0 + size.width, 0, 1.0, 0.0},
// top right tri
{0, 0 + size.height, 0.0, 1.0},
{0 + size.width, 0, 1.0, 0.0},
{0 + size.width, 0 + size.height, 1.0, 1.0},
};
glBindBuffer(GL_ARRAY_BUFFER, ui_vbo);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
}
void draw_circle(UIConstraints constraints, Color *color, Alignment align) {
float radius = 0.f;
if (constraints.width > constraints.height) {
radius = constraints.height / 2.f;
} else {
radius = constraints.width / 2.f;
}
draw_quad(constraints, color, radius, align);
}
void draw_triangle(UIConstraints constraints, Color *color, Alignment align) {
use_shader(ui_shader);
if (color) {
set_vec4f(ui_shader, "aColor", color->r / 255.f, color->g / 255.f, color->b / 255.f, color->a / 255.f);
} else {
set_vec4f(ui_shader, "aColor", 0.f, 0.f, 0.f, 1.f);
}
set_mat4f(ui_shader, "projection", (float *)zephr_context.projection.m);
Vec2f pos = { 0.f, 0.f };
Sizef size = { 0.f, 0.f };
apply_constraints(constraints, &pos, &size);
apply_alignment(align, &pos, size);
Matrix4x4 model = identity();
apply_translation(&model, (Vec2f){-size.width / 2.f, -size.height / 2.f});
apply_rotation(&model, to_radians(constraints.rotation));
apply_translation(&model, (Vec2f){size.width / 2.f, size.height / 2.f});
apply_translation(&model, pos);
2023-11-21 06:45:54 +02:00
set_mat4f(font_shader, "model", (float *)model.m);
glBindVertexArray(ui_vao);
float vertices[3][4] = {
{0 + size.width, 0, 0.0, 0.0},
{0, 0, 0.0, 0.0},
{0 + size.width / 2, 0 + size.height, 0.0, 0.0},
};
glBindBuffer(GL_ARRAY_BUFFER, ui_vbo);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
}
void draw_text(const char* text, int font_size, UIConstraints constraints, Color *color, Alignment alignment) {
use_shader(font_shader);
if (color) {
set_vec4f(font_shader, "textColor", color->r / 255.f, color->g / 255.f, color->b / 255.f, color->a / 255.f);
} else {
set_vec4f(font_shader, "textColor", 0.f, 0.f, 0.f, 1.f);
}
set_mat4f(font_shader, "projection", (float *)zephr_context.projection.m);
Vec2f pos = { 0.f, 0.f };
Sizef size = {0};
apply_constraints(constraints, &pos, &size);
Sizef text_size = calculate_text_size(text, FONT_PIXEL_SIZE);
float font_scale = (float)font_size / FONT_PIXEL_SIZE * size.width;
apply_alignment(alignment, &pos, (Sizef){ text_size.width * font_scale, text_size.height * font_scale });
Matrix4x4 model = identity();
apply_scale(&model, (Sizef){font_scale, font_scale});
apply_translation(&model, (Vec2f){-text_size.width * font_scale / 2.f, -text_size.height * font_scale / 2.f});
apply_rotation(&model, to_radians(constraints.rotation));
apply_translation(&model, (Vec2f){text_size.width *font_scale / 2.f, text_size.height * font_scale / 2.f});
apply_translation(&model, pos);
set_mat4f(font_shader, "model", (float *)model.m);
int max_bearing_h = 0;
for (uint i = 0; i < strlen(text); i++) {
Character ch = characters[(int)text[i]];
max_bearing_h = max(max_bearing_h, ch.bearing.height);
}
float first_char_bearing_w = characters[(int)text[0]].bearing.width;
glActiveTexture(GL_TEXTURE0);
glBindVertexArray(font_vao);
// we use the original text and character sizes in the loop and then we just
// scale up or down the model matrix to get the desired font size.
// this way everything works out fine and we get to transform the text using the
// model matrix
int c = 0;
int x = 0;
while (text[c] != '\0') {
Character ch = characters[(int)text[c]];
// subtract the bearing width of the first character to remove the extra space
// at the start of the text and move every char to the left by that width
float xpos = (x + (ch.bearing.width - first_char_bearing_w));
float ypos = (text_size.height - ch.bearing.height - (text_size.height - max_bearing_h));
float vertices[6][4] = {
{xpos, ypos + ch.size.height, 0.0, 1.0},
{xpos, ypos, 0.0, 0.0},
{xpos + ch.size.width, ypos, 1.0, 0.0},
{xpos, ypos + ch.size.height, 0.0, 1.0},
{xpos + ch.size.width, ypos, 1.0, 0.0},
{xpos + ch.size.width, ypos + ch.size.height, 1.0, 1.0},
};
glBindTexture(GL_TEXTURE_2D, ch.texture_id);
glBindBuffer(GL_ARRAY_BUFFER, font_vbo);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glDrawArrays(GL_TRIANGLES, 0, 6);
x += (ch.advance >> 6);
c++;
}
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
}
/* enum Element { */
/* Rect, */
/* Circle, */
/* }; */
/* struct UIElement { */
/* Vec2 pos; */
/* Size size; */
/* Color *color; */
/* enum Element type; */
/* }; */
/* struct UIText { */
/* Vec2 pos; */
/* float scale; */
/* Color *color; */
/* const char* text; */
/* }; */
/* struct UIElement *create_element(Vec2 pos, Size size, Color *color, enum Element type) { */
/* struct UIElement *element = malloc(sizeof(struct UIElement)); */
/* element->pos = pos; */
/* element->size = size; */
/* element->color = color; */
/* element->type = type; */
/* return element; */
/* } */