#include "render/ui.h" #include "core/platform.h" #include "render/shader.h" #include #include #include "math/linalg.h" #define UI_MARGIN 15.0f bool Ui_init(Ui *ui, const char *font_asset, float font_size) { memset(ui, 0, sizeof(*ui)); if (!TTF_Init()) { SDL_Log("TTF_Init failed: %s", SDL_GetError()); return false; } ui->ttf_ready = true; char path[1024]; if (!Platform_assetPath(font_asset, path, sizeof(path)) || !(ui->font = TTF_OpenFont(path, font_size))) { SDL_Log("Cannot open font %s: %s", font_asset, SDL_GetError()); Ui_shutdown(ui); return false; } ui->program = Shader_load("shaders/ui.vert", "shaders/ui.frag"); if (!ui->program) { Ui_shutdown(ui); return false; } ui->u_screen_size = glGetUniformLocation(ui->program, "screenSize"); ui->u_tex = glGetUniformLocation(ui->program, "tex"); ui->u_text_color = glGetUniformLocation(ui->program, "textColor"); ui->u_mode = glGetUniformLocation(ui->program, "mode"); ui->u_rect_size = glGetUniformLocation(ui->program, "rectSize"); ui->u_boil = glGetUniformLocation(ui->program, "boilTime"); ui->u_rim = glGetUniformLocation(ui->program, "rim"); glGenVertexArrays(1, &ui->vao); glGenBuffers(1, &ui->vbo); glBindVertexArray(ui->vao); glBindBuffer(GL_ARRAY_BUFFER, ui->vbo); glBufferData(GL_ARRAY_BUFFER, 6 * 4 * sizeof(float), NULL, GL_DYNAMIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), NULL); glEnableVertexAttribArray(0); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)(2 * sizeof(float))); glEnableVertexAttribArray(1); glBindVertexArray(0); for (int i = 0; i < UI_LABELS; i++) glGenTextures(1, &ui->labels[i].texture); return true; } void Ui_shutdown(Ui *ui) { for (int i = 0; i < UI_LABELS; i++) if (ui->labels[i].texture) glDeleteTextures(1, &ui->labels[i].texture); if (ui->vbo) glDeleteBuffers(1, &ui->vbo); if (ui->vao) glDeleteVertexArrays(1, &ui->vao); Shader_destroy(&ui->program); if (ui->font) TTF_CloseFont(ui->font); if (ui->ttf_ready) TTF_Quit(); memset(ui, 0, sizeof(*ui)); } /* Renders text into the texture. Only called when the text changes. */ static bool uploadText(Ui *ui, UiLabel *label, const char *text) { const SDL_Color white = { 255, 255, 255, 255 }; /* coverage ends up in alpha */ SDL_Surface *rendered = TTF_RenderText_Blended(ui->font, text, 0, white); if (!rendered) return false; SDL_Surface *rgba = SDL_ConvertSurface(rendered, SDL_PIXELFORMAT_RGBA32); SDL_DestroySurface(rendered); if (!rgba) return false; glBindTexture(GL_TEXTURE_2D, label->texture); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glPixelStorei(GL_UNPACK_ROW_LENGTH, rgba->pitch / 4); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, rgba->w, rgba->h, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba->pixels); glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); glBindTexture(GL_TEXTURE_2D, 0); label->width = rgba->w; label->height = rgba->h; SDL_DestroySurface(rgba); return true; } /* Label with its texture up to date, or NULL if there is nothing to draw. */ static UiLabel *prepare(Ui *ui, int index, const char *text) { if (index < 0 || index >= UI_LABELS) return NULL; UiLabel *label = &ui->labels[index]; if (strcmp(text, label->text) != 0) { if (!uploadText(ui, label, text)) return NULL; SDL_strlcpy(label->text, text, sizeof(label->text)); } return label->width > 0 ? label : NULL; } /* Ink and paper */ static const float INK[3] = { 0.10f, 0.07f, 0.14f }; static const float PAPER[3] = { 0.95f, 0.90f, 0.78f }; void Ui_setTime(Ui *ui, float seconds) { ui->boil_time = floorf(seconds * 12.0f) / 12.0f; /* "on twos", like the pictures */ } /* A textured (mode 0) or painted (mode 1) quad. */ static void drawQuad(Ui *ui, GLuint texture, float x, float y, float w, float h, int mode, const float color[4], float rim, int screen_w, int screen_h) { const float quad[] = { x, y, 0.0f, 1.0f, x + w, y, 1.0f, 1.0f, x + w, y + h, 1.0f, 0.0f, x, y, 0.0f, 1.0f, x + w, y + h, 1.0f, 0.0f, x, y + h, 0.0f, 0.0f, }; glDisable(GL_DEPTH_TEST); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glUseProgram(ui->program); glUniform2f(ui->u_screen_size, (float)screen_w, (float)screen_h); glUniform4f(ui->u_text_color, color[0], color[1], color[2], color[3]); glUniform1i(ui->u_mode, mode); glUniform2f(ui->u_rect_size, w, h); glUniform1f(ui->u_boil, ui->boil_time); glUniform1f(ui->u_rim, rim); glUniform1i(ui->u_tex, 0); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture); glBindVertexArray(ui->vao); glBindBuffer(GL_ARRAY_BUFFER, ui->vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_DYNAMIC_DRAW); glDrawArrays(GL_TRIANGLES, 0, 6); glBindVertexArray(0); glDisable(GL_BLEND); glEnable(GL_DEPTH_TEST); } static void drawLabelColored(Ui *ui, const UiLabel *label, float x, float y, int screen_w, int screen_h, const float rgb[3], float alpha) { const float color[4] = { rgb[0], rgb[1], rgb[2], alpha }; drawQuad(ui, label->texture, x, y, (float)label->width, (float)label->height, 0, color, 0.0f, screen_w, screen_h); } /* Ink letters, gone over twice a hair apart so the strokes are a bit fuller. */ static void drawLabel(Ui *ui, const UiLabel *label, float x, float y, int screen_w, int screen_h) { drawLabelColored(ui, label, x, y, screen_w, screen_h, INK, 1.0f); drawLabelColored(ui, label, x + 0.6f, y, screen_w, screen_h, INK, 1.0f); } void Ui_drawPatch(Ui *ui, float x, float y, float w, float h, float r, float g, float b, float a, float rim, int screen_w, int screen_h) { const float color[4] = { r, g, b, a }; drawQuad(ui, ui->labels[0].texture, x, y, w, h, 1, color, rim, screen_w, screen_h); } void Ui_drawTextLeft(Ui *ui, int index, const char *text, float x, float y, int screen_w, int screen_h) { const UiLabel *label = prepare(ui, index, text); if (label) drawLabel(ui, label, x, y, screen_w, screen_h); } /* Ink letters on a faint, thin paper glow: just enough to read them on dark paint, never wider than the strokes themselves. */ static void drawWithHalo(Ui *ui, const UiLabel *label, float x, float y, int screen_w, int screen_h) { static const float OFFSETS[][2] = { { -0.8f, -0.8f }, { 0.8f, 0.8f }, { -0.8f, 0.8f }, { 0.8f, -0.8f } }; for (unsigned k = 0; k < sizeof(OFFSETS) / sizeof(OFFSETS[0]); k++) drawLabelColored(ui, label, x + OFFSETS[k][0], y + OFFSETS[k][1], screen_w, screen_h, PAPER, 0.45f); drawLabel(ui, label, x, y, screen_w, screen_h); } void Ui_drawTextAt(Ui *ui, int index, const char *text, float x, float y, int screen_w, int screen_h) { const UiLabel *label = prepare(ui, index, text); if (!label) return; float left = clampf(x - (float)label->width * 0.5f, UI_MARGIN, (float)screen_w - (float)label->width - UI_MARGIN); drawWithHalo(ui, label, left, y, screen_w, screen_h); } void Ui_drawText(Ui *ui, int index, const char *text, UiCorner corner, int screen_w, int screen_h) { const UiLabel *label = prepare(ui, index, text); if (!label) return; const float w = (float)label->width, h = (float)label->height; float x = corner == UI_TOP_RIGHT ? (float)screen_w - w - UI_MARGIN : UI_MARGIN; float y = (float)screen_h - h - UI_MARGIN; if (corner == UI_CENTER) { /* a bit below the middle of the screen */ x = ((float)screen_w - w) * 0.5f; y = (float)screen_h * 0.35f; } drawWithHalo(ui, label, x, y, screen_w, screen_h); } void Ui_drawTextLine(Ui *ui, int index, const char *text, int line, UiCorner side, int screen_w, int screen_h) { const UiLabel *label = prepare(ui, index, text); if (!label) return; const float h = (float)label->height; if (side == UI_BOTTOM_LEFT) { drawWithHalo(ui, label, UI_MARGIN, UI_MARGIN + h * 1.1f * (float)line, screen_w, screen_h); return; } float x = side == UI_TOP_RIGHT ? (float)screen_w - (float)label->width - UI_MARGIN : UI_MARGIN; drawWithHalo(ui, label, x, (float)screen_h - UI_MARGIN - h * 1.1f * (float)(line + 1), screen_w, screen_h); }