From 6008ebf5ed031fd0af1b13586cb3ca7208d93327 Mon Sep 17 00:00:00 2001 From: asrael Date: Sat, 4 Oct 2025 11:55:04 -0500 Subject: [PATCH] add ui module --- demo/ui_demo.fnl | 20 +++ pxl8.sh | 3 +- src/lua/pxl8.lua | 32 ++++ src/pxl8.c | 120 ++++++++++++--- src/pxl8_font.c | 8 +- src/pxl8_gfx.c | 364 +++++++++++++++++++++++++--------------------- src/pxl8_gfx.h | 3 +- src/pxl8_script.c | 79 ++++++---- src/pxl8_script.h | 2 + src/pxl8_types.h | 13 ++ src/pxl8_ui.c | 216 +++++++++++++++++++++++++++ src/pxl8_ui.h | 58 ++++++++ 12 files changed, 703 insertions(+), 215 deletions(-) create mode 100644 demo/ui_demo.fnl create mode 100644 src/pxl8_ui.c create mode 100644 src/pxl8_ui.h diff --git a/demo/ui_demo.fnl b/demo/ui_demo.fnl new file mode 100644 index 0000000..cd4e9e3 --- /dev/null +++ b/demo/ui_demo.fnl @@ -0,0 +1,20 @@ +(local pxl8 (require :pxl8)) + +(var button-clicks 0) + +(global init (fn [] + (pxl8.load_palette "palettes/gruvbox.ase"))) + +(global update (fn [_dt])) + +(global frame (fn [] + (pxl8.clr 1) + + (when pxl8.ui + (when (pxl8.ui_window_begin "UI Demo" 20 20 280 150) + (pxl8.ui_layout_row 1 0 0) + (pxl8.ui_label "Welcome to some window UI!") + (pxl8.ui_label (.. "Clicks: " button-clicks)) + (when (pxl8.ui_button "Click me!") + (set button-clicks (+ button-clicks 1))) + (pxl8.ui_window_end))))) diff --git a/pxl8.sh b/pxl8.sh index fb99eeb..0a3d9f5 100755 --- a/pxl8.sh +++ b/pxl8.sh @@ -378,7 +378,7 @@ case "$COMMAND" in EXECUTABLE="$BINDIR/pxl8" - LIB_SOURCE_FILES="lib/linenoise/linenoise.c lib/miniz/miniz.c" + LIB_SOURCE_FILES="lib/linenoise/linenoise.c lib/microui/src/microui.c lib/miniz/miniz.c" PXL8_SOURCE_FILES=" src/pxl8.c @@ -392,6 +392,7 @@ case "$COMMAND" in src/pxl8_script.c src/pxl8_tilemap.c src/pxl8_tilesheet.c + src/pxl8_ui.c src/pxl8_vfx.c " diff --git a/src/lua/pxl8.lua b/src/lua/pxl8.lua index 1f4dbcc..289ad4d 100644 --- a/src/lua/pxl8.lua +++ b/src/lua/pxl8.lua @@ -329,5 +329,37 @@ end pxl8.gfx = gfx pxl8.input = input +pxl8.ui = _pxl8_ui + +function pxl8.bounds(x, y, w, h) + return ffi.new("pxl8_bounds", {x = x, y = y, w = w, h = h}) +end + +function pxl8.ui_window_begin(title, x, y, w, h, options) + local rect = ffi.new("pxl8_bounds", {x = x, y = y, w = w, h = h}) + return C.pxl8_ui_window_begin(_pxl8_ui, title, rect, options or 0) +end + +function pxl8.ui_window_end() + C.pxl8_ui_window_end(_pxl8_ui) +end + +function pxl8.ui_button(label) + return C.pxl8_ui_button(_pxl8_ui, label) +end + +function pxl8.ui_label(text) + C.pxl8_ui_label(_pxl8_ui, text) +end + +function pxl8.ui_layout_row(item_count, widths, height) + local widths_array = widths + if type(widths) == "table" then + widths_array = ffi.new("int[?]", #widths, widths) + elseif type(widths) == "number" then + widths_array = ffi.new("int[1]", widths) + end + C.pxl8_ui_layout_row(_pxl8_ui, item_count, widths_array, height) +end return pxl8 diff --git a/src/pxl8.c b/src/pxl8.c index ffad11f..565cf87 100644 --- a/src/pxl8.c +++ b/src/pxl8.c @@ -18,6 +18,7 @@ #include "pxl8_macros.h" #include "pxl8_script.h" #include "pxl8_types.h" +#include "pxl8_ui.h" #define PXL8_MAX_REPL_COMMANDS 4096 @@ -41,7 +42,9 @@ typedef struct pxl8_state { pxl8_repl_state repl; pxl8_resolution resolution; pxl8_script* script; + pxl8_ui* ui; + f32 current_fps; f32 fps_timer; i32 frame_count; u64 last_time; @@ -50,6 +53,7 @@ typedef struct pxl8_state { bool repl_mode; bool running; bool script_loaded; + bool show_fps; char script_path[256]; pxl8_input_state input; @@ -246,6 +250,13 @@ SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) { return SDL_APP_FAILURE; } + app.ui = pxl8_ui_create(app.gfx); + if (!app.ui) { + pxl8_error("Failed to create UI"); + pxl8_gfx_destroy(app.gfx); + return SDL_APP_FAILURE; + } + app.script = pxl8_script_create(); if (!app.script) { pxl8_error("Failed to initialize scripting: %s", pxl8_script_get_last_error(app.script)); @@ -283,6 +294,7 @@ SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) { pxl8_script_set_gfx(app.script, app.gfx); pxl8_script_set_input(app.script, &app.input); + pxl8_script_set_ui(app.script, app.ui); if (app.script_path[0] != '\0') { pxl8_result result = pxl8_script_load_main(app.script, app.script_path); @@ -317,11 +329,8 @@ SDL_AppResult SDL_AppIterate(void* appstate) { app->last_time = current_time; app->time += dt; - if (app->fps_timer >= 3.0f) { - if (!app->repl_mode) { - f32 avg_fps = app->frame_count / app->fps_timer; - pxl8_info("FPS: %.1f", avg_fps); - } + if (app->fps_timer >= 1.0f) { + app->current_fps = app->frame_count / app->fps_timer; app->frame_count = 0; app->fps_timer = 0.0f; } @@ -339,6 +348,29 @@ SDL_AppResult SDL_AppIterate(void* appstate) { } } + if (app->ui) { + pxl8_ui_frame_begin(app->ui); + + for (i32 i = 0; i < 3; i++) { + if (app->input.mouse_buttons[i] && app->input.mouse_buttons_pressed[i]) { + pxl8_ui_input_mousedown(app->ui, app->input.mouse_x, app->input.mouse_y, i + 1); + } + if (!app->input.mouse_buttons[i]) { + pxl8_ui_input_mouseup(app->ui, app->input.mouse_x, app->input.mouse_y, i + 1); + } + } + pxl8_ui_input_mousemove(app->ui, app->input.mouse_x, app->input.mouse_y); + + for (i32 key = 0; key < 256; key++) { + if (app->input.keys_pressed[key]) { + pxl8_ui_input_keydown(app->ui, key); + } + if (!app->input.keys[key] && app->input.keys_pressed[key]) { + pxl8_ui_input_keyup(app->ui, key); + } + } + } + if (app->script_loaded) { pxl8_script_call_function_f32(app->script, "update", dt); @@ -348,10 +380,10 @@ SDL_AppResult SDL_AppIterate(void* appstate) { } } else { pxl8_clr(app->gfx, 32); - + i32 render_width, render_height; pxl8_gfx_get_resolution_dimensions(app->resolution, &render_width, &render_height); - + for (i32 y = 0; y < render_height; y += 24) { for (i32 x = 0; x < render_width; x += 32) { u32 color = ((x / 32) + (y / 24) + (i32)(app->time * 2)) % 8; @@ -359,22 +391,29 @@ SDL_AppResult SDL_AppIterate(void* appstate) { } } } - + i32 render_width, render_height; pxl8_gfx_get_resolution_dimensions(app->resolution, &render_width, &render_height); - f32 scale = fminf(bounds.w / (f32)render_width, bounds.h / (f32)render_height); - i32 scaled_width = (i32)(render_width * scale); - i32 scaled_height = (i32)(render_height * scale); - i32 offset_x = (bounds.w - scaled_width) / 2; - i32 offset_y = (bounds.h - scaled_height) / 2; - - pxl8_gfx_viewport(app->gfx, offset_x, offset_y, scaled_width, scaled_height); + if (app->show_fps && app->current_fps > 0.0f) { + char fps_text[32]; + SDL_snprintf(fps_text, sizeof(fps_text), "FPS: %d", (i32)(app->current_fps + 0.5f)); + pxl8_text(app->gfx, fps_text, render_width - 80, 10, 15); + } + + if (app->ui) { + pxl8_ui_frame_end(app->ui); + } + + pxl8_gfx_set_viewport(app->gfx, pxl8_gfx_viewport(bounds, render_width, render_height)); pxl8_gfx_upload_framebuffer(app->gfx); pxl8_gfx_upload_atlas(app->gfx); pxl8_gfx_present(app->gfx); - + SDL_memset(app->input.keys_pressed, 0, sizeof(app->input.keys_pressed)); + SDL_memset(app->input.mouse_buttons_pressed, 0, sizeof(app->input.mouse_buttons_pressed)); + app->input.mouse_wheel_x = 0; + app->input.mouse_wheel_y = 0; return app->running ? SDL_APP_CONTINUE : SDL_APP_SUCCESS; } @@ -392,7 +431,11 @@ SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) { app->running = false; return SDL_APP_SUCCESS; } - + + if (event->key.key == SDLK_F3) { + app->show_fps = !app->show_fps; + } + SDL_Keycode key = event->key.key; if (key < 256) { if (!app->input.keys[key]) { @@ -411,8 +454,46 @@ SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) { } break; } + + case SDL_EVENT_MOUSE_BUTTON_DOWN: { + u8 button = event->button.button - 1; + if (button < 3) { + if (!app->input.mouse_buttons[button]) { + app->input.mouse_buttons_pressed[button] = true; + } + app->input.mouse_buttons[button] = true; + } + break; + } + + case SDL_EVENT_MOUSE_BUTTON_UP: { + u8 button = event->button.button - 1; + if (button < 3) { + app->input.mouse_buttons[button] = false; + } + break; + } + + case SDL_EVENT_MOUSE_MOTION: { + i32 window_mouse_x = (i32)event->motion.x; + i32 window_mouse_y = (i32)event->motion.y; + + i32 render_width, render_height; + pxl8_gfx_get_resolution_dimensions(app->resolution, &render_width, &render_height); + pxl8_viewport vp = pxl8_gfx_viewport(pxl8_gfx_get_bounds(app->gfx), render_width, render_height); + + app->input.mouse_x = (i32)((window_mouse_x - vp.offset_x) / vp.scale); + app->input.mouse_y = (i32)((window_mouse_y - vp.offset_y) / vp.scale); + break; + } + + case SDL_EVENT_MOUSE_WHEEL: { + app->input.mouse_wheel_x = (i32)event->wheel.x; + app->input.mouse_wheel_y = (i32)event->wheel.y; + break; + } } - + return SDL_APP_CONTINUE; } @@ -431,6 +512,9 @@ void SDL_AppQuit(void* appstate, SDL_AppResult result) { app->cart = NULL; } pxl8_script_destroy(app->script); + if (app->ui) { + pxl8_ui_destroy(app->ui); + } pxl8_gfx_destroy(app->gfx); } diff --git a/src/pxl8_font.c b/src/pxl8_font.c index 08f26ba..f896432 100644 --- a/src/pxl8_font.c +++ b/src/pxl8_font.c @@ -31,14 +31,14 @@ pxl8_result pxl8_font_create_atlas(const pxl8_font* font, u8** atlas_data, i32* for (i32 y = 0; y < glyph->height && y < font->default_height; y++) { for (i32 x = 0; x < glyph->width && x < font->default_width; x++) { - i32 glyph_idx = y * 8 + x; i32 atlas_idx = (atlas_y + y) * (*atlas_width) + (atlas_x + x); - + if (glyph->format == PXL8_FONT_FORMAT_INDEXED) { - u8 pixel_byte = glyph->data.indexed[glyph_idx / 8]; - u8 pixel_bit = (pixel_byte >> (7 - (glyph_idx % 8))) & 1; + u8 pixel_byte = glyph->data.indexed[y]; + u8 pixel_bit = (pixel_byte >> x) & 1; (*atlas_data)[atlas_idx] = pixel_bit ? 255 : 0; } else { + i32 glyph_idx = y * 8 + x; u32 rgba_pixel = glyph->data.rgba[glyph_idx]; (*atlas_data)[atlas_idx] = (rgba_pixel >> 24) & 0xFF; } diff --git a/src/pxl8_gfx.c b/src/pxl8_gfx.c index 16b56bf..9d6ba8c 100644 --- a/src/pxl8_gfx.c +++ b/src/pxl8_gfx.c @@ -6,11 +6,37 @@ #include "pxl8_ase.h" #include "pxl8_blit.h" +#include "pxl8_font.h" #include "pxl8_gfx.h" #include "pxl8_macros.h" #include "pxl8_math.h" #include "pxl8_types.h" +static inline void pxl8_color_unpack(u32 color, u8* r, u8* g, u8* b, u8* a) { + *r = color & 0xFF; + *g = (color >> 8) & 0xFF; + *b = (color >> 16) & 0xFF; + *a = (color >> 24) & 0xFF; +} + +static inline u32 pxl8_color_pack(u8 r, u8 g, u8 b, u8 a) { + return r | (g << 8) | (b << 16) | (a << 24); +} + +static inline u8 pxl8_color_lerp_channel(u8 c1, u8 c2, f32 t) { + return c1 + (i32)((c2 - c1) * t); +} + +pxl8_viewport pxl8_gfx_viewport(pxl8_bounds bounds, i32 width, i32 height) { + pxl8_viewport vp = {0}; + vp.scale = fminf(bounds.w / (f32)width, bounds.h / (f32)height); + vp.scaled_width = (i32)(width * vp.scale); + vp.scaled_height = (i32)(height * vp.scale); + vp.offset_x = (bounds.w - vp.scaled_width) / 2; + vp.offset_y = (bounds.h - vp.scaled_height) / 2; + return vp; +} + typedef struct pxl8_atlas_entry { char path[256]; u32 sprite_id; @@ -44,10 +70,7 @@ struct pxl8_gfx { u32 sprite_frame_width; u32 sprite_frames_per_row; - i32 viewport_height; - i32 viewport_width; - i32 viewport_x; - i32 viewport_y; + pxl8_viewport viewport; bool backface_culling; pxl8_mat4 model; @@ -191,10 +214,11 @@ pxl8_gfx* pxl8_gfx_create(pxl8_color_mode mode, pxl8_resolution resolution, cons } } - gfx->viewport_x = 0; - gfx->viewport_y = 0; - gfx->viewport_width = gfx->framebuffer_width; - gfx->viewport_height = gfx->framebuffer_height; + gfx->viewport.offset_x = 0; + gfx->viewport.offset_y = 0; + gfx->viewport.scaled_width = gfx->framebuffer_width; + gfx->viewport.scaled_height = gfx->framebuffer_height; + gfx->viewport.scale = 1.0f; gfx->backface_culling = true; gfx->model = pxl8_mat4_identity(); @@ -241,7 +265,6 @@ void pxl8_gfx_destroy(pxl8_gfx* gfx) { SDL_free(gfx); } -// resource loading pxl8_result pxl8_gfx_init_atlas(pxl8_gfx* gfx, u32 width, u32 height) { if (!gfx || !gfx->initialized) return PXL8_ERROR_INVALID_ARGUMENT; @@ -417,55 +440,53 @@ pxl8_result pxl8_gfx_load_font_atlas(pxl8_gfx* gfx) { return PXL8_OK; } -// rendering pipeline +static void pxl8_upload_indexed_texture(SDL_Texture* texture, const u8* indexed, const u32* palette, u32 palette_size, i32 width, i32 height, u32 default_color) { + static u32* rgba_buffer = NULL; + static size_t buffer_size = 0; + size_t needed_size = width * height; + + if (buffer_size < needed_size) { + rgba_buffer = (u32*)SDL_realloc(rgba_buffer, needed_size * 4); + buffer_size = needed_size; + } + + if (!rgba_buffer) return; + + for (i32 i = 0; i < width * height; i++) { + u8 index = indexed[i]; + rgba_buffer[i] = (index < palette_size) ? palette[index] : default_color; + } + + SDL_UpdateTexture(texture, NULL, rgba_buffer, width * 4); +} + void pxl8_gfx_upload_framebuffer(pxl8_gfx* gfx) { if (!gfx || !gfx->initialized || !gfx->framebuffer_texture) return; - + if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { - SDL_UpdateTexture(gfx->framebuffer_texture, NULL, gfx->framebuffer, + SDL_UpdateTexture(gfx->framebuffer_texture, NULL, gfx->framebuffer, gfx->framebuffer_width * 4); } else { - static u32* rgba_buffer = NULL; - static size_t buffer_size = 0; - size_t needed_size = gfx->framebuffer_width * gfx->framebuffer_height; - - if (buffer_size < needed_size) { - rgba_buffer = (u32*)SDL_realloc(rgba_buffer, needed_size * 4); - buffer_size = needed_size; - } - - if (!rgba_buffer) return; - - for (i32 i = 0; i < gfx->framebuffer_width * gfx->framebuffer_height; i++) { - u8 index = gfx->framebuffer[i]; - rgba_buffer[i] = (index < gfx->palette_size) ? gfx->palette[index] : 0xFF000000; - } - - SDL_UpdateTexture(gfx->framebuffer_texture, NULL, rgba_buffer, - gfx->framebuffer_width * 4); + pxl8_upload_indexed_texture(gfx->framebuffer_texture, gfx->framebuffer, + gfx->palette, gfx->palette_size, + gfx->framebuffer_width, gfx->framebuffer_height, + 0xFF000000); } } void pxl8_gfx_upload_atlas(pxl8_gfx* gfx) { if (!gfx || !gfx->initialized || !gfx->sprite_atlas_texture || !gfx->atlas_dirty) return; - + if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { - SDL_UpdateTexture(gfx->sprite_atlas_texture, NULL, gfx->atlas, + SDL_UpdateTexture(gfx->sprite_atlas_texture, NULL, gfx->atlas, gfx->sprite_atlas_width * 4); } else { - u32* rgba_buffer = (u32*)SDL_malloc(gfx->sprite_atlas_width * gfx->sprite_atlas_height * 4); - if (!rgba_buffer) return; - - for (u32 i = 0; i < gfx->sprite_atlas_width * gfx->sprite_atlas_height; i++) { - u8 index = gfx->atlas[i]; - rgba_buffer[i] = (index < gfx->palette_size) ? gfx->palette[index] : 0x00000000; - } - - SDL_UpdateTexture(gfx->sprite_atlas_texture, NULL, rgba_buffer, - gfx->sprite_atlas_width * 4); - SDL_free(rgba_buffer); + pxl8_upload_indexed_texture(gfx->sprite_atlas_texture, gfx->atlas, + gfx->palette, gfx->palette_size, + gfx->sprite_atlas_width, gfx->sprite_atlas_height, + 0x00000000); } - + gfx->atlas_dirty = false; pxl8_debug("Atlas uploaded to GPU"); } @@ -483,19 +504,15 @@ void pxl8_gfx_present(pxl8_gfx* gfx) { SDL_RenderPresent(gfx->renderer); } -void pxl8_gfx_viewport(pxl8_gfx* gfx, i32 x, i32 y, i32 width, i32 height) { +void pxl8_gfx_set_viewport(pxl8_gfx* gfx, pxl8_viewport vp) { if (!gfx) return; - gfx->viewport_x = x; - gfx->viewport_y = y; - gfx->viewport_width = width; - gfx->viewport_height = height; + gfx->viewport = vp; } void pxl8_gfx_project(pxl8_gfx* gfx, f32 left, f32 right, f32 top, f32 bottom) { (void)gfx; (void)left; (void)right; (void)top; (void)bottom; } -// drawing primitives void pxl8_clr(pxl8_gfx* gfx, u32 color) { if (!gfx || !gfx->framebuffer) return; @@ -571,12 +588,26 @@ void pxl8_rect(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, u32 color) { pxl8_line(gfx, x, y + h - 1, x, y, color); } +static inline void pxl8_pixel_unchecked(pxl8_gfx* gfx, i32 x, i32 y, u32 color) { + i32 idx = y * gfx->framebuffer_width + x; + if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { + ((u32*)gfx->framebuffer)[idx] = color; + } else { + gfx->framebuffer[idx] = color & 0xFF; + } +} + void pxl8_rect_fill(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, u32 color) { - if (!gfx) return; - - for (i32 py = y; py < y + h; py++) { - for (i32 px = x; px < x + w; px++) { - pxl8_pixel(gfx, px, py, color); + if (!gfx || !gfx->framebuffer) return; + + i32 x0 = (x < 0) ? 0 : x; + i32 y0 = (y < 0) ? 0 : y; + i32 x1 = (x + w > gfx->framebuffer_width) ? gfx->framebuffer_width : x + w; + i32 y1 = (y + h > gfx->framebuffer_height) ? gfx->framebuffer_height : y + h; + + for (i32 py = y0; py < y1; py++) { + for (i32 px = x0; px < x1; px++) { + pxl8_pixel_unchecked(gfx, px, py, color); } } } @@ -609,20 +640,65 @@ void pxl8_circle(pxl8_gfx* gfx, i32 cx, i32 cy, i32 radius, u32 color) { } void pxl8_circle_fill(pxl8_gfx* gfx, i32 cx, i32 cy, i32 radius, u32 color) { - if (!gfx) return; - - for (i32 y = -radius; y <= radius; y++) { - for (i32 x = -radius; x <= radius; x++) { + if (!gfx || !gfx->framebuffer) return; + + i32 x0 = (cx - radius < 0) ? -cx : -radius; + i32 y0 = (cy - radius < 0) ? -cy : -radius; + i32 x1 = (cx + radius >= gfx->framebuffer_width) ? gfx->framebuffer_width - cx - 1 : radius; + i32 y1 = (cy + radius >= gfx->framebuffer_height) ? gfx->framebuffer_height - cy - 1 : radius; + + for (i32 y = y0; y <= y1; y++) { + for (i32 x = x0; x <= x1; x++) { if (x * x + y * y <= radius * radius) { - pxl8_pixel(gfx, cx + x, cy + y, color); + pxl8_pixel_unchecked(gfx, cx + x, cy + y, color); } } } } void pxl8_text(pxl8_gfx* gfx, const char* text, i32 x, i32 y, u32 color) { - if (!gfx || !text) return; - (void)x; (void)y; (void)color; + if (!gfx || !text || !gfx->framebuffer) return; + + const pxl8_font* font = &pxl8_default_font; + i32 cursor_x = x; + i32 cursor_y = y; + + for (const char* c = text; *c; c++) { + const pxl8_glyph* glyph = pxl8_font_find_glyph(font, (u32)*c); + if (!glyph) continue; + + for (i32 gy = 0; gy < glyph->height; gy++) { + for (i32 gx = 0; gx < glyph->width; gx++) { + i32 px = cursor_x + gx; + i32 py = cursor_y + gy; + + if (px < 0 || px >= gfx->framebuffer_width || + py < 0 || py >= gfx->framebuffer_height) continue; + + u8 pixel_bit = 0; + + if (glyph->format == PXL8_FONT_FORMAT_INDEXED) { + u8 pixel_byte = glyph->data.indexed[gy]; + pixel_bit = (pixel_byte >> gx) & 1; + } else { + i32 glyph_idx = gy * 8 + gx; + u32 rgba_pixel = glyph->data.rgba[glyph_idx]; + pixel_bit = ((rgba_pixel >> 24) & 0xFF) > 128 ? 1 : 0; + } + + if (pixel_bit) { + i32 fb_idx = py * gfx->framebuffer_width + px; + if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { + ((u32*)gfx->framebuffer)[fb_idx] = color; + } else { + gfx->framebuffer[fb_idx] = (u8)color; + } + } + } + } + + cursor_x += font->default_width; + } } @@ -681,7 +757,6 @@ void pxl8_sprite(pxl8_gfx* gfx, u32 sprite_id, i32 x, i32 y, i32 w, i32 h) { } } -// palette effects void pxl8_gfx_cycle_palette(pxl8_gfx* gfx, u8 start, u8 count, i32 step) { if (!gfx || !gfx->palette || count == 0) return; @@ -707,82 +782,63 @@ void pxl8_gfx_swap_palette(pxl8_gfx* gfx, u8 start, u8 count, u32* new_colors) { void pxl8_gfx_fade_palette(pxl8_gfx* gfx, u8 start, u8 count, f32 amount, u32 target_color) { if (!gfx || !gfx->palette || count == 0) return; - + if (amount < 0.0f) amount = 0.0f; if (amount > 1.0f) amount = 1.0f; - - u8 target_r = target_color & 0xFF; - u8 target_g = (target_color >> 8) & 0xFF; - u8 target_b = (target_color >> 16) & 0xFF; - u8 target_a = (target_color >> 24) & 0xFF; - + + u8 target_r, target_g, target_b, target_a; + pxl8_color_unpack(target_color, &target_r, &target_g, &target_b, &target_a); + for (u8 i = 0; i < count && (start + i) < gfx->palette_size; i++) { - u32 current = gfx->palette[start + i]; - u8 cur_r = current & 0xFF; - u8 cur_g = (current >> 8) & 0xFF; - u8 cur_b = (current >> 16) & 0xFF; - u8 cur_a = (current >> 24) & 0xFF; - - u8 new_r = cur_r + (i32)((target_r - cur_r) * amount); - u8 new_g = cur_g + (i32)((target_g - cur_g) * amount); - u8 new_b = cur_b + (i32)((target_b - cur_b) * amount); - u8 new_a = cur_a + (i32)((target_a - cur_a) * amount); - - gfx->palette[start + i] = new_r | (new_g << 8) | (new_b << 16) | (new_a << 24); + u8 cur_r, cur_g, cur_b, cur_a; + pxl8_color_unpack(gfx->palette[start + i], &cur_r, &cur_g, &cur_b, &cur_a); + + u8 new_r = pxl8_color_lerp_channel(cur_r, target_r, amount); + u8 new_g = pxl8_color_lerp_channel(cur_g, target_g, amount); + u8 new_b = pxl8_color_lerp_channel(cur_b, target_b, amount); + u8 new_a = pxl8_color_lerp_channel(cur_a, target_a, amount); + + gfx->palette[start + i] = pxl8_color_pack(new_r, new_g, new_b, new_a); } } void pxl8_gfx_interpolate_palettes(pxl8_gfx* gfx, u32* palette1, u32* palette2, u8 start, u8 count, f32 t) { if (!gfx || !gfx->palette || !palette1 || !palette2 || count == 0) return; - + if (t < 0.0f) t = 0.0f; if (t > 1.0f) t = 1.0f; - + for (u8 i = 0; i < count && (start + i) < gfx->palette_size; i++) { - u32 col1 = palette1[i]; - u32 col2 = palette2[i]; - - u8 r1 = col1 & 0xFF; - u8 g1 = (col1 >> 8) & 0xFF; - u8 b1 = (col1 >> 16) & 0xFF; - u8 a1 = (col1 >> 24) & 0xFF; - - u8 r2 = col2 & 0xFF; - u8 g2 = (col2 >> 8) & 0xFF; - u8 b2 = (col2 >> 16) & 0xFF; - u8 a2 = (col2 >> 24) & 0xFF; - - u8 r = r1 + (i32)((r2 - r1) * t); - u8 g = g1 + (i32)((g2 - g1) * t); - u8 b = b1 + (i32)((b2 - b1) * t); - u8 a = a1 + (i32)((a2 - a1) * t); - - gfx->palette[start + i] = r | (g << 8) | (b << 16) | (a << 24); + u8 r1, g1, b1, a1, r2, g2, b2, a2; + pxl8_color_unpack(palette1[i], &r1, &g1, &b1, &a1); + pxl8_color_unpack(palette2[i], &r2, &g2, &b2, &a2); + + u8 r = pxl8_color_lerp_channel(r1, r2, t); + u8 g = pxl8_color_lerp_channel(g1, g2, t); + u8 b = pxl8_color_lerp_channel(b1, b2, t); + u8 a = pxl8_color_lerp_channel(a1, a2, t); + + gfx->palette[start + i] = pxl8_color_pack(r, g, b, a); } } void pxl8_gfx_color_ramp(pxl8_gfx* gfx, u8 start, u8 count, u32 from_color, u32 to_color) { if (!gfx || !gfx->palette || count <= 1) return; - - u8 from_r = from_color & 0xFF; - u8 from_g = (from_color >> 8) & 0xFF; - u8 from_b = (from_color >> 16) & 0xFF; - u8 from_a = (from_color >> 24) & 0xFF; - - u8 to_r = to_color & 0xFF; - u8 to_g = (to_color >> 8) & 0xFF; - u8 to_b = (to_color >> 16) & 0xFF; - u8 to_a = (to_color >> 24) & 0xFF; - + + u8 from_r, from_g, from_b, from_a; + u8 to_r, to_g, to_b, to_a; + pxl8_color_unpack(from_color, &from_r, &from_g, &from_b, &from_a); + pxl8_color_unpack(to_color, &to_r, &to_g, &to_b, &to_a); + for (u8 i = 0; i < count && (start + i) < gfx->palette_size; i++) { f32 t = (f32)i / (f32)(count - 1); - - u8 r = from_r + (i32)((to_r - from_r) * t); - u8 g = from_g + (i32)((to_g - from_g) * t); - u8 b = from_b + (i32)((to_b - from_b) * t); - u8 a = from_a + (i32)((to_a - from_a) * t); - - gfx->palette[start + i] = r | (g << 8) | (b << 16) | (a << 24); + + u8 r = pxl8_color_lerp_channel(from_r, to_r, t); + u8 g = pxl8_color_lerp_channel(from_g, to_g, t); + u8 b = pxl8_color_lerp_channel(from_b, to_b, t); + u8 a = pxl8_color_lerp_channel(from_a, to_a, t); + + gfx->palette[start + i] = pxl8_color_pack(r, g, b, a); } } @@ -895,6 +951,26 @@ void pxl8_3d_draw_line_3d(pxl8_gfx* gfx, pxl8_vec3 p0, pxl8_vec3 p1, u32 color) pxl8_line(gfx, x0, y0, x1, y1, color); } +static inline void pxl8_fill_scanline(pxl8_gfx* gfx, i32 y, i32 xs, i32 xe, f32 z0, f32 z1, u32 color) { + if (y < 0 || y >= gfx->framebuffer_height) return; + if (xs > xe) { + i32 tmp = xs; xs = xe; xe = tmp; + } + + for (i32 x = xs; x <= xe; x++) { + if (x >= 0 && x < gfx->framebuffer_width) { + f32 t = (xe == xs) ? 0.0f : (f32)(x - xs) / (f32)(xe - xs); + f32 z = z0 + t * (z1 - z0); + + i32 idx = y * gfx->zbuffer_width + x; + if (z <= gfx->zbuffer[idx]) { + gfx->zbuffer[idx] = z; + pxl8_pixel(gfx, x, y, color); + } + } + } +} + static void pxl8_draw_flat_bottom_triangle( pxl8_gfx* gfx, i32 x0, i32 y0, f32 z0, @@ -912,28 +988,7 @@ static void pxl8_draw_flat_bottom_triangle( f32 cur_x2 = (f32)x0; for (i32 y = y0; y <= y1; y++) { - if (y >= 0 && y < gfx->framebuffer_height) { - i32 xs = (i32)cur_x1; - i32 xe = (i32)cur_x2; - - if (xs > xe) { - i32 tmp = xs; xs = xe; xe = tmp; - } - - for (i32 x = xs; x <= xe; x++) { - if (x >= 0 && x < gfx->framebuffer_width) { - f32 t = (xe == xs) ? 0.0f : (f32)(x - xs) / (f32)(xe - xs); - f32 z = z0 + t * (z1 - z0); - - i32 idx = y * gfx->zbuffer_width + x; - if (z <= gfx->zbuffer[idx]) { - gfx->zbuffer[idx] = z; - pxl8_pixel(gfx, x, y, color); - } - } - } - } - + pxl8_fill_scanline(gfx, y, (i32)cur_x1, (i32)cur_x2, z0, z1, color); cur_x1 += inv_slope_1; cur_x2 += inv_slope_2; } @@ -956,28 +1011,7 @@ static void pxl8_draw_flat_top_triangle( f32 cur_x2 = (f32)x2; for (i32 y = y2; y > y0; y--) { - if (y >= 0 && y < gfx->framebuffer_height) { - i32 xs = (i32)cur_x1; - i32 xe = (i32)cur_x2; - - if (xs > xe) { - i32 tmp = xs; xs = xe; xe = tmp; - } - - for (i32 x = xs; x <= xe; x++) { - if (x >= 0 && x < gfx->framebuffer_width) { - f32 t = (xe == xs) ? 0.0f : (f32)(x - xs) / (f32)(xe - xs); - f32 z = z0 + t * (z1 - z0); - - i32 idx = y * gfx->zbuffer_width + x; - if (z <= gfx->zbuffer[idx]) { - gfx->zbuffer[idx] = z; - pxl8_pixel(gfx, x, y, color); - } - } - } - } - + pxl8_fill_scanline(gfx, y, (i32)cur_x1, (i32)cur_x2, z0, z1, color); cur_x1 -= inv_slope_1; cur_x2 -= inv_slope_2; } diff --git a/src/pxl8_gfx.h b/src/pxl8_gfx.h index 9263a56..b6108db 100644 --- a/src/pxl8_gfx.h +++ b/src/pxl8_gfx.h @@ -71,7 +71,8 @@ void pxl8_gfx_present(pxl8_gfx* gfx); void pxl8_gfx_project(pxl8_gfx* gfx, f32 left, f32 right, f32 top, f32 bottom); void pxl8_gfx_upload_atlas(pxl8_gfx* gfx); void pxl8_gfx_upload_framebuffer(pxl8_gfx* gfx); -void pxl8_gfx_viewport(pxl8_gfx* gfx, i32 x, i32 y, i32 width, i32 height); +pxl8_viewport pxl8_gfx_viewport(pxl8_bounds bounds, i32 width, i32 height); +void pxl8_gfx_set_viewport(pxl8_gfx* gfx, pxl8_viewport vp); void pxl8_gfx_color_ramp(pxl8_gfx* gfx, u8 start, u8 count, u32 from_color, u32 to_color); void pxl8_gfx_cycle_palette(pxl8_gfx* gfx, u8 start, u8 count, i32 step); diff --git a/src/pxl8_script.c b/src/pxl8_script.c index 6d3a5ea..ce90121 100644 --- a/src/pxl8_script.c +++ b/src/pxl8_script.c @@ -11,12 +11,14 @@ #include "pxl8_macros.h" #include "pxl8_script.h" +#include "pxl8_ui.h" #include "pxl8_vfx.h" struct pxl8_script { lua_State* L; pxl8_gfx* gfx; pxl8_input_state* input; + pxl8_ui* ui; char last_error[1024]; char main_path[256]; char watch_dir[256]; @@ -35,7 +37,7 @@ static const char* pxl8_ffi_cdefs = "typedef float f32;\n" "typedef double f64;\n" "typedef struct pxl8_gfx pxl8_gfx;\n" -"typedef struct { int x, y, w, h; } pxl8_rect;\n" +"typedef struct { int x, y, w, h; } pxl8_bounds;\n" "typedef struct { int x, y; } pxl8_point;\n" "\n" "i32 pxl8_gfx_get_height(pxl8_gfx* ctx);\n" @@ -145,7 +147,30 @@ static const char* pxl8_ffi_cdefs = "pxl8_mat4 pxl8_mat4_rotate_y(float angle);\n" "pxl8_mat4 pxl8_mat4_rotate_z(float angle);\n" "pxl8_mat4 pxl8_mat4_scale(float x, float y, float z);\n" -"pxl8_mat4 pxl8_mat4_translate(float x, float y, float z);\n"; +"pxl8_mat4 pxl8_mat4_translate(float x, float y, float z);\n" +"\n" +"typedef struct pxl8_ui pxl8_ui;\n" +"typedef struct { unsigned char bg_color; unsigned int sprite_id; int corner_size; int edge_size; int padding; } pxl8_frame_theme;\n" +"typedef struct { bool enabled; const char* label; } pxl8_menu_item;\n" +"pxl8_ui* pxl8_ui_create(pxl8_gfx* gfx);\n" +"void pxl8_ui_destroy(pxl8_ui* ui);\n" +"void pxl8_ui_frame_begin(pxl8_ui* ui);\n" +"void pxl8_ui_frame_end(pxl8_ui* ui);\n" +"void pxl8_ui_input_keydown(pxl8_ui* ui, int key);\n" +"void pxl8_ui_input_keyup(pxl8_ui* ui, int key);\n" +"void pxl8_ui_input_mousedown(pxl8_ui* ui, int x, int y, int button);\n" +"void pxl8_ui_input_mousemove(pxl8_ui* ui, int x, int y);\n" +"void pxl8_ui_input_mouseup(pxl8_ui* ui, int x, int y, int button);\n" +"void pxl8_ui_input_scroll(pxl8_ui* ui, int x, int y);\n" +"void pxl8_ui_input_text(pxl8_ui* ui, const char* text);\n" +"bool pxl8_ui_button(pxl8_ui* ui, const char* label);\n" +"void pxl8_ui_label(pxl8_ui* ui, const char* text);\n" +"void pxl8_ui_layout_row(pxl8_ui* ui, int item_count, const int* widths, int height);\n" +"int pxl8_ui_menu(pxl8_ui* ui, pxl8_menu_item* items, int item_count);\n" +"void pxl8_ui_panel(pxl8_ui* ui, pxl8_bounds rect, pxl8_frame_theme* theme);\n" +"bool pxl8_ui_window_begin(pxl8_ui* ui, const char* title, pxl8_bounds rect, int options);\n" +"void pxl8_ui_window_end(pxl8_ui* ui);\n" +"pxl8_frame_theme pxl8_ui_theme_default(void);\n"; void pxl8_lua_info(const char* msg) { pxl8_info("%s", msg); @@ -249,12 +274,19 @@ void pxl8_script_set_input(pxl8_script* script, pxl8_input_state* input) { } } -pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename) { - if (!script || !script->L || !filename) return PXL8_ERROR_NULL_POINTER; +void pxl8_script_set_ui(pxl8_script* script, pxl8_ui* ui) { + if (!script) return; + script->ui = ui; + if (script->L && ui) { + lua_pushlightuserdata(script->L, ui); + lua_setglobal(script->L, "_pxl8_ui"); + } +} +static pxl8_result pxl8_script_prepare_path(pxl8_script* script, const char* filename, char** out_basename) { char* filename_copy = strdup(filename); char* last_slash = strrchr(filename_copy, '/'); - const char* basename = filename; + *out_basename = (char*)filename; if (last_slash) { *last_slash = '\0'; @@ -263,12 +295,22 @@ pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename) { if (script_dir && original_cwd) { chdir(script_dir); pxl8_script_set_cart_path(script, script_dir, original_cwd); - basename = last_slash + 1; + *out_basename = strdup(last_slash + 1); } free(script_dir); free(original_cwd); } + free(filename_copy); + return PXL8_OK; +} + +pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename) { + if (!script || !script->L || !filename) return PXL8_ERROR_NULL_POINTER; + + char* basename; + pxl8_script_prepare_path(script, filename, &basename); + pxl8_result result = PXL8_OK; if (luaL_dofile(script->L, basename) != 0) { pxl8_script_set_error(script, lua_tostring(script->L, -1)); @@ -278,8 +320,7 @@ pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename) { script->last_error[0] = '\0'; } - free(filename_copy); - + if (basename != filename) free(basename); return result; } @@ -320,28 +361,14 @@ void pxl8_script_set_cart_path(pxl8_script* script, const char* cart_path, const pxl8_result pxl8_script_run_fennel_file(pxl8_script* script, const char* filename) { if (!script || !script->L || !filename) return PXL8_ERROR_NULL_POINTER; - char* filename_copy = strdup(filename); - char* last_slash = strrchr(filename_copy, '/'); - const char* basename = filename; - - if (last_slash) { - *last_slash = '\0'; - char* script_dir = realpath(filename_copy, NULL); - char* original_cwd = getcwd(NULL, 0); - if (script_dir && original_cwd) { - chdir(script_dir); - pxl8_script_set_cart_path(script, script_dir, original_cwd); - basename = last_slash + 1; - } - free(script_dir); - free(original_cwd); - } + char* basename; + pxl8_script_prepare_path(script, filename, &basename); lua_getglobal(script->L, "fennel"); if (lua_isnil(script->L, -1)) { pxl8_script_set_error(script, "Fennel not loaded"); lua_pop(script->L, 1); - free(filename_copy); + if (basename != filename) free(basename); return PXL8_ERROR_SCRIPT_ERROR; } @@ -358,7 +385,7 @@ pxl8_result pxl8_script_run_fennel_file(pxl8_script* script, const char* filenam } lua_pop(script->L, 1); - free(filename_copy); + if (basename != filename) free(basename); return result; } diff --git a/src/pxl8_script.h b/src/pxl8_script.h index e6a5fe8..aa3479f 100644 --- a/src/pxl8_script.h +++ b/src/pxl8_script.h @@ -2,6 +2,7 @@ #include "pxl8_gfx.h" #include "pxl8_types.h" +#include "pxl8_ui.h" typedef struct pxl8_script pxl8_script; @@ -16,6 +17,7 @@ const char* pxl8_script_get_last_error(pxl8_script* script); void pxl8_script_set_cart_path(pxl8_script* script, const char* cart_path, const char* original_cwd); void pxl8_script_set_gfx(pxl8_script* script, pxl8_gfx* gfx); void pxl8_script_set_input(pxl8_script* script, pxl8_input_state* input); +void pxl8_script_set_ui(pxl8_script* script, pxl8_ui* ui); pxl8_result pxl8_script_call_function(pxl8_script* script, const char* name); pxl8_result pxl8_script_call_function_f32(pxl8_script* script, const char* name, f32 arg); diff --git a/src/pxl8_types.h b/src/pxl8_types.h index 1ce0cf8..1eb0c4f 100644 --- a/src/pxl8_types.h +++ b/src/pxl8_types.h @@ -66,8 +66,21 @@ typedef struct pxl8_bounds { typedef struct pxl8_input_state { bool keys[256]; bool keys_pressed[256]; + + bool mouse_buttons[3]; + bool mouse_buttons_pressed[3]; + i32 mouse_wheel_x; + i32 mouse_wheel_y; + i32 mouse_x; + i32 mouse_y; } pxl8_input_state; typedef struct pxl8_point { i32 x, y; } pxl8_point; + +typedef struct pxl8_viewport { + i32 offset_x, offset_y; + i32 scaled_width, scaled_height; + f32 scale; +} pxl8_viewport; diff --git a/src/pxl8_ui.c b/src/pxl8_ui.c new file mode 100644 index 0000000..6f58623 --- /dev/null +++ b/src/pxl8_ui.c @@ -0,0 +1,216 @@ +#include "pxl8_ui.h" +#include "../lib/microui/src/microui.h" +#include "pxl8_font.h" + +#include +#include +#include + +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; +} + +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; + u8 color = rc->color.r; + pxl8_rect_fill(ui->gfx, rc->rect.x, rc->rect.y, rc->rect.w, rc->rect.h, color); + break; + } + case MU_COMMAND_TEXT: { + mu_TextCommand* tc = (mu_TextCommand*)cmd; + u8 color = tc->color.r; + pxl8_text(ui->gfx, tc->str, tc->pos.x, tc->pos.y, color); + break; + } + case MU_COMMAND_CLIP: { + break; + } + case MU_COMMAND_ICON: { + 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; + + 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; +} + +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); +} + +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; +} diff --git a/src/pxl8_ui.h b/src/pxl8_ui.h new file mode 100644 index 0000000..c2fc749 --- /dev/null +++ b/src/pxl8_ui.h @@ -0,0 +1,58 @@ +#pragma once + +#include "pxl8_gfx.h" +#include "pxl8_types.h" + +typedef struct pxl8_ui pxl8_ui; + +typedef struct pxl8_frame_theme { + u8 bg_color; + u32 sprite_id; + i32 corner_size; + i32 edge_size; + i32 padding; +} pxl8_frame_theme; + +typedef struct pxl8_menu_item { + bool enabled; + const char* label; +} pxl8_menu_item; + +typedef enum pxl8_ui_options { + PXL8_UI_NOCLOSE = 1 << 0, + PXL8_UI_NOFRAME = 1 << 1, + PXL8_UI_NORESIZE = 1 << 2, + PXL8_UI_NOTITLE = 1 << 3 +} pxl8_ui_options; + +#ifdef __cplusplus +extern "C" { +#endif + +pxl8_ui* pxl8_ui_create(pxl8_gfx* gfx); +void pxl8_ui_destroy(pxl8_ui* ui); + +void pxl8_ui_frame_begin(pxl8_ui* ui); +void pxl8_ui_frame_end(pxl8_ui* ui); + +void pxl8_ui_input_keydown(pxl8_ui* ui, i32 key); +void pxl8_ui_input_keyup(pxl8_ui* ui, i32 key); +void pxl8_ui_input_mousedown(pxl8_ui* ui, i32 x, i32 y, i32 button); +void pxl8_ui_input_mousemove(pxl8_ui* ui, i32 x, i32 y); +void pxl8_ui_input_mouseup(pxl8_ui* ui, i32 x, i32 y, i32 button); +void pxl8_ui_input_scroll(pxl8_ui* ui, i32 x, i32 y); +void pxl8_ui_input_text(pxl8_ui* ui, const char* text); + +bool pxl8_ui_button(pxl8_ui* ui, const char* label); +void pxl8_ui_label(pxl8_ui* ui, const char* text); +void pxl8_ui_layout_row(pxl8_ui* ui, i32 item_count, const i32* widths, i32 height); +i32 pxl8_ui_menu(pxl8_ui* ui, pxl8_menu_item* items, i32 item_count); +void pxl8_ui_panel(pxl8_ui* ui, pxl8_bounds rect, pxl8_frame_theme* theme); +bool pxl8_ui_window_begin(pxl8_ui* ui, const char* title, pxl8_bounds rect, i32 options); +void pxl8_ui_window_end(pxl8_ui* ui); + +pxl8_frame_theme pxl8_ui_theme_default(void); + +#ifdef __cplusplus +} +#endif