2025-10-04 11:55:04 -05:00
|
|
|
#include "pxl8_ui.h"
|
|
|
|
|
#include "../lib/microui/src/microui.h"
|
|
|
|
|
#include "pxl8_font.h"
|
|
|
|
|
|
|
|
|
|
#include <SDL3/SDL.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
|
|
struct pxl8_ui {
|
|
|
|
|
pxl8_gfx* gfx;
|
|
|
|
|
mu_Context mu_ctx;
|
|
|
|
|
i32 font_height;
|
|
|
|
|
i32 font_width;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static int mu_text_width(mu_Font font, const char* str, int len) {
|
|
|
|
|
const pxl8_font* f = (const pxl8_font*)font;
|
|
|
|
|
if (!f) return 0;
|
|
|
|
|
|
|
|
|
|
if (len < 0) {
|
|
|
|
|
len = (int)strlen(str);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
i32 width = 0;
|
|
|
|
|
for (int i = 0; i < len; i++) {
|
|
|
|
|
const pxl8_glyph* glyph = pxl8_font_find_glyph(f, (u32)str[i]);
|
|
|
|
|
if (glyph) {
|
|
|
|
|
width += f->default_width;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return width;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int mu_text_height(mu_Font font) {
|
|
|
|
|
const pxl8_font* f = (const pxl8_font*)font;
|
|
|
|
|
if (!f) return 8;
|
|
|
|
|
return f->default_height;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 18:14:07 -05:00
|
|
|
static void pxl8_ui_render_icon(pxl8_gfx* gfx, i32 id, i32 x, i32 y, i32 w, i32 h, u8 color) {
|
|
|
|
|
switch (id) {
|
|
|
|
|
case 2: {
|
|
|
|
|
i32 cx = x + w / 2;
|
|
|
|
|
i32 cy = y + h / 2;
|
|
|
|
|
i32 size = (w < h ? w : h) / 3;
|
|
|
|
|
pxl8_line(gfx, cx - size, cy, cx, cy + size, color);
|
|
|
|
|
pxl8_line(gfx, cx, cy + size, cx + size, cy - size, color);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case 1: {
|
|
|
|
|
i32 cx = x + w / 2;
|
|
|
|
|
i32 cy = y + h / 2;
|
|
|
|
|
i32 size = (w < h ? w : h) / 4;
|
|
|
|
|
pxl8_line(gfx, cx - size, cy - size, cx + size, cy + size, color);
|
|
|
|
|
pxl8_line(gfx, cx + size, cy - size, cx - size, cy + size, color);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-04 11:55:04 -05:00
|
|
|
static void pxl8_ui_render_commands(pxl8_ui* ui) {
|
|
|
|
|
mu_Command* cmd = NULL;
|
|
|
|
|
while (mu_next_command(&ui->mu_ctx, &cmd)) {
|
|
|
|
|
switch (cmd->type) {
|
|
|
|
|
case MU_COMMAND_RECT: {
|
|
|
|
|
mu_RectCommand* rc = (mu_RectCommand*)cmd;
|
2025-10-06 18:14:07 -05:00
|
|
|
pxl8_rect_fill(ui->gfx, rc->rect.x, rc->rect.y, rc->rect.w, rc->rect.h, rc->color.r);
|
2025-10-04 11:55:04 -05:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case MU_COMMAND_TEXT: {
|
|
|
|
|
mu_TextCommand* tc = (mu_TextCommand*)cmd;
|
2025-10-06 18:14:07 -05:00
|
|
|
pxl8_text(ui->gfx, tc->str, tc->pos.x, tc->pos.y, tc->color.r);
|
2025-10-04 11:55:04 -05:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case MU_COMMAND_CLIP: {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case MU_COMMAND_ICON: {
|
2025-10-06 18:14:07 -05:00
|
|
|
mu_IconCommand* ic = (mu_IconCommand*)cmd;
|
|
|
|
|
pxl8_ui_render_icon(ui->gfx, ic->id, ic->rect.x, ic->rect.y, ic->rect.w, ic->rect.h, ic->color.r);
|
2025-10-04 11:55:04 -05:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void pxl8_ui_draw_9slice(pxl8_gfx* gfx, pxl8_bounds rect, pxl8_frame_theme* theme) {
|
|
|
|
|
if (theme->sprite_id == 0) return;
|
|
|
|
|
|
|
|
|
|
i32 cs = theme->corner_size;
|
|
|
|
|
i32 es = theme->edge_size;
|
|
|
|
|
|
|
|
|
|
pxl8_sprite(gfx, theme->sprite_id, rect.x, rect.y, cs, cs);
|
|
|
|
|
pxl8_sprite(gfx, theme->sprite_id, rect.x + rect.w - cs, rect.y, cs, cs);
|
|
|
|
|
pxl8_sprite(gfx, theme->sprite_id, rect.x, rect.y + rect.h - cs, cs, cs);
|
|
|
|
|
pxl8_sprite(gfx, theme->sprite_id, rect.x + rect.w - cs, rect.y + rect.h - cs, cs, cs);
|
|
|
|
|
|
|
|
|
|
for (i32 x = cs; x < rect.w - cs; x += es) {
|
|
|
|
|
i32 w = (x + es > rect.w - cs) ? (rect.w - cs - x) : es;
|
|
|
|
|
pxl8_sprite(gfx, theme->sprite_id, rect.x + x, rect.y, w, es);
|
|
|
|
|
pxl8_sprite(gfx, theme->sprite_id, rect.x + x, rect.y + rect.h - es, w, es);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (i32 y = cs; y < rect.h - cs; y += es) {
|
|
|
|
|
i32 h = (y + es > rect.h - cs) ? (rect.h - cs - y) : es;
|
|
|
|
|
pxl8_sprite(gfx, theme->sprite_id, rect.x, rect.y + y, es, h);
|
|
|
|
|
pxl8_sprite(gfx, theme->sprite_id, rect.x + rect.w - es, rect.y + y, es, h);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pxl8_ui* pxl8_ui_create(pxl8_gfx* gfx) {
|
|
|
|
|
if (!gfx) return NULL;
|
|
|
|
|
|
|
|
|
|
pxl8_ui* ui = (pxl8_ui*)malloc(sizeof(pxl8_ui));
|
|
|
|
|
if (!ui) return NULL;
|
|
|
|
|
|
|
|
|
|
memset(ui, 0, sizeof(pxl8_ui));
|
|
|
|
|
ui->gfx = gfx;
|
|
|
|
|
ui->font_width = 8;
|
|
|
|
|
ui->font_height = 8;
|
|
|
|
|
|
|
|
|
|
mu_init(&ui->mu_ctx);
|
|
|
|
|
ui->mu_ctx.style->font = (mu_Font)&pxl8_default_font;
|
|
|
|
|
ui->mu_ctx.text_height = mu_text_height;
|
|
|
|
|
ui->mu_ctx.text_width = mu_text_width;
|
|
|
|
|
|
2025-10-06 18:14:07 -05:00
|
|
|
ui->mu_ctx.style->colors[0] = (mu_Color){15, 0, 0, 255};
|
|
|
|
|
ui->mu_ctx.style->colors[1] = (mu_Color){8, 0, 0, 255};
|
|
|
|
|
ui->mu_ctx.style->colors[2] = (mu_Color){1, 0, 0, 255};
|
|
|
|
|
ui->mu_ctx.style->colors[3] = (mu_Color){2, 0, 0, 255};
|
|
|
|
|
ui->mu_ctx.style->colors[4] = (mu_Color){15, 0, 0, 255};
|
|
|
|
|
ui->mu_ctx.style->colors[5] = (mu_Color){0, 0, 0, 0};
|
|
|
|
|
ui->mu_ctx.style->colors[6] = (mu_Color){7, 0, 0, 255};
|
|
|
|
|
ui->mu_ctx.style->colors[7] = (mu_Color){8, 0, 0, 255};
|
|
|
|
|
ui->mu_ctx.style->colors[8] = (mu_Color){10, 0, 0, 255};
|
|
|
|
|
ui->mu_ctx.style->colors[9] = (mu_Color){2, 0, 0, 255};
|
|
|
|
|
ui->mu_ctx.style->colors[10] = (mu_Color){3, 0, 0, 255};
|
|
|
|
|
ui->mu_ctx.style->colors[11] = (mu_Color){10, 0, 0, 255};
|
|
|
|
|
ui->mu_ctx.style->colors[12] = (mu_Color){7, 0, 0, 255};
|
|
|
|
|
ui->mu_ctx.style->colors[13] = (mu_Color){8, 0, 0, 255};
|
|
|
|
|
|
2025-10-04 11:55:04 -05:00
|
|
|
return ui;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_ui_destroy(pxl8_ui* ui) {
|
|
|
|
|
if (!ui) return;
|
|
|
|
|
free(ui);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_ui_frame_begin(pxl8_ui* ui) {
|
|
|
|
|
if (!ui) return;
|
|
|
|
|
mu_begin(&ui->mu_ctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_ui_frame_end(pxl8_ui* ui) {
|
|
|
|
|
if (!ui) return;
|
|
|
|
|
mu_end(&ui->mu_ctx);
|
|
|
|
|
pxl8_ui_render_commands(ui);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_ui_input_keydown(pxl8_ui* ui, i32 key) {
|
|
|
|
|
if (!ui) return;
|
|
|
|
|
mu_input_keydown(&ui->mu_ctx, key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_ui_input_keyup(pxl8_ui* ui, i32 key) {
|
|
|
|
|
if (!ui) return;
|
|
|
|
|
mu_input_keyup(&ui->mu_ctx, key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_ui_input_mousedown(pxl8_ui* ui, i32 x, i32 y, i32 button) {
|
|
|
|
|
if (!ui) return;
|
|
|
|
|
mu_input_mousedown(&ui->mu_ctx, x, y, button);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_ui_input_mousemove(pxl8_ui* ui, i32 x, i32 y) {
|
|
|
|
|
if (!ui) return;
|
|
|
|
|
mu_input_mousemove(&ui->mu_ctx, x, y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_ui_input_mouseup(pxl8_ui* ui, i32 x, i32 y, i32 button) {
|
|
|
|
|
if (!ui) return;
|
|
|
|
|
mu_input_mouseup(&ui->mu_ctx, x, y, button);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_ui_input_scroll(pxl8_ui* ui, i32 x, i32 y) {
|
|
|
|
|
if (!ui) return;
|
|
|
|
|
mu_input_scroll(&ui->mu_ctx, x, y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_ui_input_text(pxl8_ui* ui, const char* text) {
|
|
|
|
|
if (!ui || !text) return;
|
|
|
|
|
mu_input_text(&ui->mu_ctx, text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool pxl8_ui_button(pxl8_ui* ui, const char* label) {
|
|
|
|
|
if (!ui) return false;
|
|
|
|
|
return mu_button(&ui->mu_ctx, label) & MU_RES_SUBMIT;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 18:14:07 -05:00
|
|
|
bool pxl8_ui_checkbox(pxl8_ui* ui, const char* label, bool* state) {
|
|
|
|
|
if (!ui || !state || !label) return false;
|
|
|
|
|
|
|
|
|
|
mu_Context* ctx = &ui->mu_ctx;
|
|
|
|
|
mu_push_id(ctx, label, (int)strlen(label));
|
|
|
|
|
|
|
|
|
|
mu_Id id = mu_get_id(ctx, label, (int)strlen(label));
|
|
|
|
|
mu_Rect r = mu_layout_next(ctx);
|
|
|
|
|
mu_Rect box = mu_rect(r.x, r.y, r.h, r.h);
|
|
|
|
|
|
|
|
|
|
int had_focus_before = (ctx->focus == id);
|
|
|
|
|
mu_update_control(ctx, id, r, 0);
|
|
|
|
|
int has_focus_after = (ctx->focus == id);
|
|
|
|
|
int mouseover = mu_mouse_over(ctx, r);
|
|
|
|
|
|
|
|
|
|
int res = 0;
|
|
|
|
|
int int_state = *state ? 1 : 0;
|
|
|
|
|
|
|
|
|
|
if (had_focus_before && !has_focus_after && !ctx->mouse_down && mouseover) {
|
|
|
|
|
res |= MU_RES_CHANGE;
|
|
|
|
|
int_state = !int_state;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mu_draw_control_frame(ctx, id, box, MU_COLOR_BASE, 0);
|
|
|
|
|
if (int_state) {
|
|
|
|
|
mu_draw_icon(ctx, MU_ICON_CHECK, box, ctx->style->colors[MU_COLOR_TEXT]);
|
|
|
|
|
}
|
|
|
|
|
r = mu_rect(r.x + box.w, r.y, r.w - box.w, r.h);
|
|
|
|
|
mu_draw_control_text(ctx, label, r, MU_COLOR_TEXT, 0);
|
|
|
|
|
|
|
|
|
|
mu_pop_id(ctx);
|
|
|
|
|
|
|
|
|
|
*state = int_state != 0;
|
|
|
|
|
return res & MU_RES_CHANGE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_ui_indent(pxl8_ui* ui, i32 amount) {
|
|
|
|
|
if (!ui) return;
|
|
|
|
|
mu_Layout* layout = &ui->mu_ctx.layout_stack.items[ui->mu_ctx.layout_stack.idx - 1];
|
|
|
|
|
layout->indent += amount;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-04 11:55:04 -05:00
|
|
|
void pxl8_ui_label(pxl8_ui* ui, const char* text) {
|
|
|
|
|
if (!ui || !text) return;
|
|
|
|
|
mu_label(&ui->mu_ctx, text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_ui_layout_row(pxl8_ui* ui, i32 item_count, const i32* widths, i32 height) {
|
|
|
|
|
if (!ui) return;
|
|
|
|
|
mu_layout_row(&ui->mu_ctx, item_count, widths, height);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
i32 pxl8_ui_menu(pxl8_ui* ui, pxl8_menu_item* items, i32 item_count) {
|
|
|
|
|
if (!ui || !items) return -1;
|
|
|
|
|
|
|
|
|
|
i32 selected = -1;
|
|
|
|
|
for (i32 i = 0; i < item_count; i++) {
|
|
|
|
|
if (!items[i].enabled) {
|
|
|
|
|
mu_label(&ui->mu_ctx, items[i].label);
|
|
|
|
|
} else if (mu_button(&ui->mu_ctx, items[i].label) & MU_RES_SUBMIT) {
|
|
|
|
|
selected = i;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return selected;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_ui_panel(pxl8_ui* ui, pxl8_bounds rect, pxl8_frame_theme* theme) {
|
|
|
|
|
if (!ui || !theme) return;
|
|
|
|
|
|
|
|
|
|
pxl8_rect_fill(ui->gfx, rect.x, rect.y, rect.w, rect.h, theme->bg_color);
|
|
|
|
|
pxl8_ui_draw_9slice(ui->gfx, rect, theme);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool pxl8_ui_window_begin(pxl8_ui* ui, const char* title, pxl8_bounds rect, i32 options) {
|
|
|
|
|
if (!ui) return false;
|
|
|
|
|
mu_Rect r = { rect.x, rect.y, rect.w, rect.h };
|
|
|
|
|
return mu_begin_window_ex(&ui->mu_ctx, title, r, options);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_ui_window_end(pxl8_ui* ui) {
|
|
|
|
|
if (!ui) return;
|
|
|
|
|
mu_end_window(&ui->mu_ctx);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 18:14:07 -05:00
|
|
|
void pxl8_ui_window_set_open(pxl8_ui* ui, const char* title, bool open) {
|
|
|
|
|
if (!ui || !title) return;
|
|
|
|
|
mu_Container* win = mu_get_container(&ui->mu_ctx, title);
|
|
|
|
|
if (win) {
|
|
|
|
|
win->open = open ? 1 : 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-04 11:55:04 -05:00
|
|
|
pxl8_frame_theme pxl8_ui_theme_default(void) {
|
|
|
|
|
pxl8_frame_theme theme = {0};
|
|
|
|
|
theme.bg_color = 0;
|
|
|
|
|
theme.corner_size = 8;
|
|
|
|
|
theme.edge_size = 8;
|
|
|
|
|
theme.padding = 4;
|
|
|
|
|
theme.sprite_id = 0;
|
|
|
|
|
return theme;
|
|
|
|
|
}
|