Nimbin[12]?Games / Entropie / entropie/src/render/ui.c

Entropie git · main

Entropie festival game; first steps

sdl c game entropie cmake · first commit 2026-10-10 · last commit 2026-11-02 (18 hours ago) · synced 10 hours ago

C 88.5% Markdown 9.1% GLSL 2.3%
git clone https://git.christianimmanuel.de/games/Entropie.gitwget https://git.christianimmanuel.de/games/Entropie/archive/Entropie.tar.gz
entropie/src/render/ui.c 8.7 KB · 215 lines raw
#include "render/ui.h"
#include "core/platform.h"
#include "render/shader.h"

#include <string.h>
#include <SDL3/SDL.h>

#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);
}