From dc96fcd771bea09d726a760266739906e95c09a6 Mon Sep 17 00:00:00 2001 From: Vasco Date: Sun, 23 Aug 2026 02:25:33 +0100 Subject: Peak 0.1.1 --- Fuse/allocators.c | 303 ++++++++++++++++++++++++++++++++ Fuse/allocators.h | 60 +++++++ Fuse/demos/fuse.slang | 31 ++++ Fuse/demos/fuse_demo.c | 280 ++++++++++++++++++++++++++++++ Fuse/fuse.c | 436 +++++++++++++++++++++++++++++++++++++++++++++++ Fuse/fuse.h | 138 +++++++++++++++ Fuse/fuse_backend.h | 42 +++++ Fuse/fuse_backend_rend.c | 0 Fuse/fuse_components.c | 3 + Fuse/fuse_hash.c | 2 + Fuse/fuse_internal.h | 63 +++++++ Fuse/fuse_positioning.c | 14 ++ Fuse/fuse_test | Bin 0 -> 35776 bytes Fuse/fuse_test.c | 25 +++ Fuse/fuse_tween.c | 68 ++++++++ 15 files changed, 1465 insertions(+) create mode 100644 Fuse/allocators.c create mode 100644 Fuse/allocators.h create mode 100644 Fuse/demos/fuse.slang create mode 100644 Fuse/demos/fuse_demo.c create mode 100644 Fuse/fuse.c create mode 100644 Fuse/fuse.h create mode 100644 Fuse/fuse_backend.h create mode 100644 Fuse/fuse_backend_rend.c create mode 100644 Fuse/fuse_components.c create mode 100644 Fuse/fuse_hash.c create mode 100644 Fuse/fuse_internal.h create mode 100644 Fuse/fuse_positioning.c create mode 100755 Fuse/fuse_test create mode 100644 Fuse/fuse_test.c create mode 100644 Fuse/fuse_tween.c (limited to 'Fuse') diff --git a/Fuse/allocators.c b/Fuse/allocators.c new file mode 100644 index 0000000..f2c6a67 --- /dev/null +++ b/Fuse/allocators.c @@ -0,0 +1,303 @@ +#pragma once + +#include "allocators.h" + +static inline bool is_power_of_two(uintptr_t x) { + return (x & (x-1)) == 0; +} + +static inline uintptr_t align_forward(uintptr_t ptr, size_t align) { + uintptr_t p, a, remainder; + assert(is_power_of_two(align)); + + p = ptr; + a = (uintptr_t)align; + remainder = p & (a-1); + + return (remainder != 0) ? p + a - remainder : p; +} + + +void +al_arena_create(Arena *a, void *backing_buffer, size_t backing_buffer_length) +{ + a->buf = (unsigned char *)backing_buffer; + a->buf_len = backing_buffer_length; + a->curr_offset = 0; + a->prev_offset = 0; +} + +void* +al_arena_alloc_align(Arena *a, size_t s, size_t align) +{ + + // Align 'curr_offset' forward to the specified alignment + uintptr_t curr_ptr = (uintptr_t)a->buf + (uintptr_t)a->curr_offset; + uintptr_t offset = align_forward(curr_ptr, align); + offset -= (uintptr_t)a->buf; // Change to relative offset + + // Check to see if the backing memory has space left + if (offset+s <= a->buf_len) { + void *ptr = &a->buf[offset]; + a->prev_offset = offset; + a->curr_offset = offset+s; + + memset(ptr, 0, s); + return ptr; + } + + return NULL; +} + +void* +al_arena_alloc(Arena *a, size_t s) { + return al_arena_alloc_align(a, s, DEFAULT_ALIGNMENT); +} + +void* +al_arena_resize_align(Arena *a, void *old_memory, size_t old_size, size_t new_size, size_t align) +{ + assert(is_power_of_two(align)); + unsigned char *old_mem = (unsigned char *)old_memory; + + if (old_mem == NULL || old_size == 0) { + return al_arena_alloc_align(a, new_size, align); + } else if (a->buf <= old_mem && old_mem < a->buf+a->buf_len) { + if (a->buf+a->prev_offset == old_mem) { + a->curr_offset = a->prev_offset + new_size; + if (new_size > old_size) { + // Zero the new memory by default + memset(&a->buf[a->curr_offset], 0, new_size-old_size); + } + return old_memory; + } else { + void *new_memory = al_arena_alloc_align(a, new_size, align); + size_t copy_size = old_size < new_size ? old_size : new_size; + memmove(new_memory, old_memory, copy_size); + return new_memory; + } + + } else { + assert(0 && "Memory is out of bounds of the buffer in this arena"); + return NULL; + } +} + +void* +al_arena_resize(Arena *a, void *old_memory, size_t old_size, size_t new_size) +{ + return al_arena_resize_align(a, old_memory, old_size, new_size, DEFAULT_ALIGNMENT); +} + +void +al_arena_free_all(Arena *a) +{ + a->curr_offset = 0; + a->prev_offset = 0; +} + +void +al_arena_destroy(Arena* a) +{ + a->buf_len = 0; + a->curr_offset = 0; + a->prev_offset = 0; +} + +void +al_stack_create(Stack *s, void *backing_buffer, size_t backing_buffer_length) +{ + s->buf = (unsigned char*) backing_buffer; + s->buf_len = backing_buffer_length; + s->offset = 0; +} + +size_t +al_calc_padding_with_header(uintptr_t ptr, uintptr_t alignment, size_t header_size) +{ + uintptr_t p, a, modulo, padding, needed_space; + + assert(is_power_of_two(alignment)); + + p = ptr; + a = alignment; + modulo = p & (a-1); // (p % a) as it assumes alignment is a power of two + + padding = 0; + needed_space = 0; + + if (modulo != 0) { // Same logic as 'align_forward' + padding = a - modulo; + } + + needed_space = (uintptr_t)header_size; + + if (padding < needed_space) { + needed_space -= padding; + + if ((needed_space & (a-1)) != 0) { + padding += a * (1+(needed_space/a)); + } else { + padding += a * (needed_space/a); + } + } + + return (size_t)padding; +} + +void* +al_stack_alloc(Stack *s, size_t len, size_t alignment) +{ + uintptr_t curr_addr, next_addr; + size_t padding; + Stack_Allocation_Header *header; + + assert(is_power_of_two(alignment)); + + if (alignment > 128) { + // As the padding is 8 bits (1 byte), the largest alignment that can + // be used is 128 bytes + alignment = 128; + } + + curr_addr = (uintptr_t)s->buf + (uintptr_t)s->offset; + padding = al_calc_padding_with_header(curr_addr, (uintptr_t)alignment, sizeof(Stack_Allocation_Header)); + if (s->offset + padding + len > s->buf_len) { + // Stack allocator is out of memory + return NULL; + } + s->offset += padding; + + next_addr = curr_addr + (uintptr_t)padding; + header = (Stack_Allocation_Header *)(next_addr - sizeof(Stack_Allocation_Header)); + header->padding = (uint8_t)padding; + + s->offset += len; + + return memset((void *)next_addr, 0, len); +} + +void +al_stack_free(Stack *s, void *ptr) +{ + if (ptr != NULL) { + uintptr_t start, end, curr_addr; + Stack_Allocation_Header *header; + size_t prev_offset; + + start = (uintptr_t)s->buf; + end = start + (uintptr_t)s->buf_len; + curr_addr = (uintptr_t)ptr; + + if (!(start <= curr_addr && curr_addr < end)) { + assert(0 && "Out of bounds memory address passed to stack allocator (free)"); + return; + } + + if (curr_addr >= start+(uintptr_t)s->offset) { + // Allow double frees + return; + } + + header = (Stack_Allocation_Header *)(curr_addr - sizeof(Stack_Allocation_Header)); + prev_offset = (size_t)(curr_addr - (uintptr_t)header->padding - start); + + s->offset = prev_offset; + } +} + +void +al_stack_free_all(Stack* s) +{ + s->offset = 0; +} + +void +al_stack_destroy(Stack* s) +{ + s->buf = 0; + s->buf_len = 0; + s->offset = 0; +} + +void +al_pool_create(Pool *p, void *buf, size_t buf_len, size_t chunk_size, size_t chunk_align) +{ + /* align start */ + uintptr_t start = (uintptr_t) buf; + uintptr_t aligned = align_forward(start, chunk_align); + buf_len -= (aligned - start); + + /* align chunk size_t */ + chunk_size = (size_t) align_forward(chunk_size, chunk_align); + + assert(chunk_size >= sizeof(Pool_Free_Node) && "Chunk size_t is too small"); + assert(buf_len >= chunk_size && "Backing buffer length is smaller than the chunk size"); + + p->buf = (unsigned char *) buf; + p->pool_size = buf_len; + p->chunk_size = chunk_size; + p->head = NULL; + + al_pool_free_all(p); +} + +void* +al_pool_alloc(Pool *p) +{ + Pool_Free_Node *node = p->head; + + assert(node != NULL && "Pool allocator has no free memory"); + + p->head = p->head->next; + + return memset(node, 0 , p->chunk_size); +} + +void +al_pool_free(Pool *p, void *ptr) +{ + Pool_Free_Node *node; + + void *start = p->buf; + void *end = &p->buf[p->pool_size]; + + if (ptr == NULL) { + return; + } + + if (!(start <= ptr && ptr < end)) { + assert(0 && "Memory is out of bounds of the buffer in this pool"); + return; + } + + node = (Pool_Free_Node *)ptr; + node->next = p->head; + p->head = node; +} + +void +al_pool_free_all(Pool *p) +{ + size_t chunk_count = p->pool_size / p->chunk_size; + size_t i; + + for (i = 0; i < chunk_count; i++) { + void *ptr = &p->buf[i * p->chunk_size]; + Pool_Free_Node *node = (Pool_Free_Node *)ptr; + + // Push free node onto thte free list + node->next = p->head; + p->head = node; + } +} + +void +al_pool_destroy(Pool *p) +{ + p->buf = NULL; + p->pool_size = 0; + p->chunk_size = 0; + p->head = NULL; +} + diff --git a/Fuse/allocators.h b/Fuse/allocators.h new file mode 100644 index 0000000..d029558 --- /dev/null +++ b/Fuse/allocators.h @@ -0,0 +1,60 @@ +#ifndef ALLOCATORS_H +#define ALLOCATORS_H + +#include +#include +#include +#include + +#ifndef DEFAULT_ALIGNMENT +#define DEFAULT_ALIGNMENT (2*sizeof(void *)) +#endif + +typedef struct Arena { + unsigned char *buf; + size_t buf_len; + size_t curr_offset; + size_t prev_offset; +} Arena; + +typedef struct { uint8_t padding; } Stack_Allocation_Header; +typedef struct Stack { + unsigned char *buf; + size_t buf_len; + size_t offset; +} Stack; + +typedef struct Pool_Free_Node Pool_Free_Node; + +typedef struct Pool { + unsigned char *buf; + size_t chunk_size; + size_t pool_size; + Pool_Free_Node *head; +} Pool; + +struct Pool_Free_Node { struct Pool_Free_Node *next; }; + +void al_arena_create(Arena *a, void *backing_buffer, size_t backing_buffer_length); +void* al_arena_alloc_align(Arena *a, size_t s, size_t align); +void* al_arena_alloc(Arena *a, size_t s); +void* al_arena_resize_align(Arena *a, void *old_memory, size_t old_size, size_t new_size, size_t align); +void* al_arena_resize(Arena *a, void *old_memory, size_t old_size, size_t new_size); +void al_arena_free_all(Arena *a); +void al_arena_destroy(Arena* a); + +void al_stack_create(Stack *s, void *backing_buffer, size_t backing_buffer_length); +size_t al_calc_padding_with_header(uintptr_t ptr, uintptr_t alignment, size_t header_size); +void* al_stack_alloc(Stack *s, size_t len, size_t alignment); +void al_stack_free(Stack *s, void *ptr); +void al_stack_free_all(Stack* s); +void al_stack_destroy(Stack* s); + +void al_pool_create(Pool *p, void *buf, size_t buf_len, size_t chunk_size, size_t chunk_align); +void* al_pool_alloc(Pool *p); +void al_pool_free(Pool *p, void *ptr); +void al_pool_free_all(Pool *p); +void al_pool_destroy(Pool *p); + + +#endif diff --git a/Fuse/demos/fuse.slang b/Fuse/demos/fuse.slang new file mode 100644 index 0000000..837a3dc --- /dev/null +++ b/Fuse/demos/fuse.slang @@ -0,0 +1,31 @@ +struct VertexInput { + [[vk::location(0)]] float2 pos : POSITION; + [[vk::location(1)]] float4 col : COLOR; +}; + +struct VertexOutput { + float4 col : COLOR; + float4 pos : SV_Position; +}; + +struct UBO { + float2 viewport; +}; + +[[vk::binding(0, 0)]] +ConstantBuffer ubo; + +[shader("vertex")] +VertexOutput vertMain(VertexInput input) +{ + VertexOutput output; + output.col = input.col; + output.pos = float4(input.pos*2.0/ubo.viewport - 1.0, 0.0, 1.0); + return output; +} + +[shader("fragment")] +float4 fragMain(VertexOutput input) : SV_Target +{ + return input.col; +} diff --git a/Fuse/demos/fuse_demo.c b/Fuse/demos/fuse_demo.c new file mode 100644 index 0000000..60128bd --- /dev/null +++ b/Fuse/demos/fuse_demo.c @@ -0,0 +1,280 @@ +#include "rend.h" +#include "rend.c" +#include "fuse.h" +#include "fuse.c" +#include +#include + +/* has to be included last because rend will ask for certain modules */ +#define PODIUM_IMPLEMENTATION +#include "podium.h" + +typedef struct { float x, y; } FuseVec2; +typedef struct { float x, y, z, w; } FuseVec4; + +#pragma pack(push, 1) +typedef struct { + FuseVec2 pos; + FuseVec4 color; +} FuseVertex; +#pragma pack(pop) + +static FuseVertex fuse_renderer_data[1024]; +static size_t fuse_renderer_idx = 0; + +struct data_t { float offset_x; float padding = 0 }; +struct data_t button_data = { .offset_x = 0, .padding = 0}; + +void stretch_button_op(void *data) { + struct data_t* ptr = (struct data_t*) data; + data->offset_x += 10; + data->padding += 10; +} + + +static inline FuseVec4 +fuse__unpack_color(uint32_t c) +{ + FuseVec4 v; + v.x = (float) ((c >> 24) & 0xFF) / 255.0f; // Red + v.y = (float) ((c >> 16) & 0xFF) / 255.0f; // Green + v.z = (float) ((c >> 8) & 0xFF) / 255.0f; // Blue + v.w = (float) ( c & 0xFF) / 255.0f; // Alpha + return v; +} + +void fuse_handler(FuseCanvas canvas, FuseCmd *cmd, void *data) { + switch (cmd->type) { + case FUSE_CMD_RECT: + FuseRect rect = cmd->rect; + FuseVec4 color = fuse__unpack_color(rect.color); + FuseVertex c0 = { + .pos = { rect.x, rect.y }, + .color = color + }; + + FuseVertex c1 = { + .pos = { rect.x + rect.w, rect.y }, + .color = color + }; + + FuseVertex c2 = { + .pos = { rect.x + rect.w, rect.y + rect.h }, + .color = color + }; + + FuseVertex c3 = { + .pos = { rect.x, rect.y + rect.h }, + .color = color + }; + + fuse_renderer_data[fuse_renderer_idx++] = c0; + fuse_renderer_data[fuse_renderer_idx++] = c1; + fuse_renderer_data[fuse_renderer_idx++] = c2; + + fuse_renderer_data[fuse_renderer_idx++] = c2; + fuse_renderer_data[fuse_renderer_idx++] = c3; + fuse_renderer_data[fuse_renderer_idx++] = c0; + break; + } +} + +#define SCREEN_WIDTH 800 +#define SCREEN_HEIGHT 600 + +int main() { + + P_Window window; + if (!p_window_open(&window, 800, 600, "fuse_demo")) { + PFATAL("Failed to open window!"); + return 1; + } + + if (!rend_init(REND_BACKEND_AUTO)) { + PFATAL("Failed to initialize backend!"); + return 1; + } + + RendRenderer renderer = rend_renderer_create(&window, true); + RendShader vert = rend_shader_from_spv_path(REND_SHADER_VERTEX, "fuse.vert.spv"); + RendShader frag = rend_shader_from_spv_path(REND_SHADER_FRAG, "fuse.frag.spv"); + + RendPipelineConfig ui_cfg = rend_pipeline_config_create( + REND_TOPOLOGY_TRIANGLE_LIST, + REND_POLYGON_MODE_FILL, + REND_CULL_MODE_BACK); + rend_pipeline_config_depth_test(ui_cfg, false); + + /* configure vertex input */ + int b0 = rend_pipeline_config_vertex_binding_add(ui_cfg, sizeof (FuseVertex), REND_INPUT_RATE_VERTEX); + rend_pipeline_config_vertex_attribute_add(ui_cfg, b0, 0, REND_FORMAT_R32G32_SFLOAT, offsetof(FuseVertex, pos)); + rend_pipeline_config_vertex_attribute_add(ui_cfg, b0, 1, REND_FORMAT_R32G32B32A32_SFLOAT, offsetof(FuseVertex, color)); + rend_pipeline_config_resource_binding_add(ui_cfg, 0, 0, REND_BUFFER_UNIFORM, REND_SHADER_VERTEX, 1); + + /* add the shaders */ + rend_pipeline_config_shader_add(ui_cfg, vert); + rend_pipeline_config_shader_add(ui_cfg, frag); + + RendPipeline ui_pipeline = rend_pipeline_create(renderer, ui_cfg); + + RendResourceSet ui_resources = rend_resource_set_create(renderer, ui_pipeline, 0); + + FuseCanvas canvas = fuse_canvas_create(1024, NULL); + + u64 frame_t = NANOS_PER_SEC / 60; + + bool running = true; + P_Event ev; + float pev = 0, px = 0, py = 0; + + float nob = 0.3; + + float target_screen[2] = { SCREEN_WIDTH, SCREEN_HEIGHT }; + rend_resource_set_write_buffer(ui_resources, 0, REND_BUFFER_UNIFORM, target_screen, 2 * sizeof *target_screen, 0); + + float real_screen_width = 0; + float real_screen_heigth = 0; + + enum UI_State { + UI_START_MENU, + UI_OPTIONS, + }; + + int ui_state = UI_START_MENU; + + while (running) { + + u64 start = p_get_time(); + + pev = 0; + /* event loop */ + while (p_window_poll_event(&window, &ev)) { + if (ev.type == P_EVENT_WINDOW_CLOSE) { + running = false; + break; + } + + switch (ev.type) { + case P_EVENT_WINDOW_RESIZE: + real_screen_width = ev.resize.width; + real_screen_heigth = ev.resize.height; + break; + case P_EVENT_POINTER: + px = ev.pointer.x; + py = ev.pointer.y; + switch (ev.pointer.state) { + case P_POINTER_PRESSED: + if (ev.pointer.button == 1 || ev.pointer.button == 0) + pev = FUSE_POINTER_PRESSED; + break; + case P_POINTER_RELEASED: + if (ev.pointer.button == 1 || ev.pointer.button == 0) + pev = FUSE_POINTER_RELEASED; + break; + default: + case P_POINTER_MOVED: + break; + } + + fuse_canvas_pointer(pev, px * SCREEN_WIDTH / real_screen_width, py * SCREEN_HEIGHT / real_screen_heigth); + break; + } + } + + /* Clears the Screen */ + fuse_canvas_clear(canvas, 0x000000); + + /* Inside a div, children no longer have fixed positions, + * instead x, y represent an offset so that they can still be animated! + * Width and height represent min width and min height instead! */ + if (ui_state == UI_START_MENU) { + // NOTE: brackets for legibility only + FUSE_LAYOUT_DIRECTION(FUSE_DIRECTION_COLUMN); + fuse_div_begin(0, 0, SCREEN_WIDTH / 4, SCREEN_HEIGHT, 0xFFFFFFaa); + { + + fuse_hovered(stretch_button_op, data); + + if (fuse_button_text("Start New Game", button_data.offset_x, 0, 100, 0, 0xFF0000FF, 0x000000FF)) { + printf("start new game"); + } + + + if (fuse_button_text("Options", 0, 0, 100, 0, 0xFF0000FF, 0x000000FF)) { + ui_state = UI_OPTIONS; + } + + + if (fuse_button_text("Credits", 0, 0, 100, 0, 0xFF0000FF, 0x000000FF)) { + printf("awesome credits"); + } + + if (fuse_button_text("Quit Game")) { + running = false; + break; + } + + fuse_hovered_clear(); + + } + fuse_div_end(); + + /* back to global layouting */ + fuse_3d_object(); + fuse_3d_point_at(); + + + } else if (ui_state == UI_OPTIONS) { + + /* Inside a flex the same x,y as offsets and width and height as minimums rules apply + * The difference is that a flex is flexible, by default it does not have a fixed size + * but you may specify minimums and maximus for it to work around. + */ + + FUSE( + .max_width = 300, + .max_height = 600, + .direction = FUSE_DIRECTION_COLUMN, + .gap = 32 + ); + + fuse_flex_begin(0, 0, SCREEN_WIDTH / 4, SCREEN_HEIGHT, 0xFFFFFFaa); + { + if (fuse_checkbox_text("VSYNC", &vsync)) { + vsync = ~vsync; + } + } + } + + fuse_slider(100, 200, 300, 600, 0xFFFF00FF, 0xFF0000FF, &nob); + + fuse_canvas_draw(canvas, fuse_handler, NULL); + + /* call renderer */ + if (rend_renderer_frame_begin(renderer)) { + rend_pipeline_bind(ui_pipeline); + rend_resource_set_bind(ui_pipeline, 0, ui_resources); + + rend_pipeline_push_data(ui_pipeline, fuse_renderer_data, fuse_renderer_idx, sizeof *fuse_renderer_data); + rend_pipeline_draw(ui_pipeline); + + rend_renderer_frame_end(renderer, NULL); + } + + fuse_renderer_idx = 0; + + u64 elapsed = p_get_time() - start; + + /* the renderer should already be forcing vsync */ + if (elapsed < frame_t) + p_sleep_ns(frame_t - elapsed); + } + + + fuse_canvas_destroy(canvas); + + rend_quit(); + + p_window_close(&window); + return 0; +} diff --git a/Fuse/fuse.c b/Fuse/fuse.c new file mode 100644 index 0000000..401b06e --- /dev/null +++ b/Fuse/fuse.c @@ -0,0 +1,436 @@ +#ifndef _FUSE_C_ +#define _FUSE_C_ + /* __________________________________________________________________________ + * | CmdType | CmdRect | CmdType | CmdRectBorder | ... + * -------------------------------------------------------------------------- + */ + + +#include "fuse.h" +#include "fuse_internal.h" +#include "fuse_backend.h" + +#include "fuse_tween.c" + +#include "allocators.c" +#include +#include + +#define FUSE_MIN(a, b) ((a < b) ? a : b) +#define FUSE_MAX(a, b) ((a > b) ? a : b) +#define FUSE_LIMIT(n, a, b) (FUSE_MAX(a, FUSE_MIN(b, n))) +#define FUSE_IS_HOVERED(x, y, w, h) (\ + f__canvas->pointer_x >= x && f__canvas->pointer_x <= x + w &&\ + f__canvas->pointer_y >= y && f__canvas->pointer_y <= y + h) + +static inline size_t fuse__element(); // "base class" of all UI widgets +static inline void fuse__cmdbuffer_push(FuseCmd cmd); +static void fuse__cmdbuffer_print(); + +static const char* fuse__cmd_name(uint8_t type); + +extern FuseCanvas +fuse_canvas_create(size_t memory_limit, FuseStyle base_style) +{ + (void) base_style; + + FuseCanvas new = malloc(sizeof *new); + memset(new, 0, sizeof *new); + + new->memory = malloc(memory_limit); + + al_arena_create(&new->arena, new->memory, memory_limit); + + size_t cmdbuffer_mem = (memory_limit * 8) / 10; + cmdbuffer_mem -= (cmdbuffer_mem % sizeof *new->cmdbuffer_ptr); // remove remainder + + FuseCmd *cmdbuffer_ptr = al_arena_alloc(&new->arena, cmdbuffer_mem); + new->cmdbuffer_ptr = cmdbuffer_ptr; + new->cmdbuffer_size = cmdbuffer_mem / sizeof (*cmdbuffer_ptr); + new->cmdbuffer_head = 0; + + size_t element_data_mem = memory_limit - cmdbuffer_mem; + void *element_data_ptr = al_arena_alloc(&new->arena, element_data_mem); + + al_arena_create(&new->element_data_arena, element_data_ptr, element_data_mem); + + fuse_canvas_set(new); + return new; +} + +extern void +fuse_canvas_set(FuseCanvas canvas) +{ + f__canvas = canvas; +} + +extern void +fuse_canvas_debug(FuseCanvas canvas, bool debug) +{ + canvas->debug = debug; +} + +extern void +fuse_canvas_clear(FuseCanvas canvas) +{ + canvas->cmdbuffer_head = 0; + canvas->current_id = 0; + canvas->current_frame++; +} + +extern void +fuse_canvas_draw(FuseCanvas canvas, FuseRendererCallback callback, void* data) +{ + for (size_t u = 0; u < f__canvas->cmdbuffer_head; u++) { + FuseCmd cmd = f__canvas->cmdbuffer_ptr[u]; + assert(cmd.type < FUSE_CMD_COUNT && "[FUSE] WARN: INVALID TYPE IN COMMAND BUFFER"); + callback(canvas, &cmd, data); + } +} + +extern void +fuse_canvas_resize(FuseCanvas canvas, float w, float h) +{ + canvas->width = w; + canvas->height = h; +} + +extern void +fuse_canvas_pointer(FuseCanvas canvas, int pointer_state, float x, float y) +{ + canvas->pointer_state = pointer_state; + canvas->pointer_x = x; + canvas->pointer_y = y; +#ifdef FUSE_DEBUG + const char *pstate[] = { + [FUSE_POINTER_NONE] = "NONE", + [FUSE_POINTER_PRESSED] = "PRESSED", + [FUSE_POINTER_RELEASED] = "RELEASED", + [FUSE_POINTER_ALT] = "ALT" + }; + printf("[DEBUG] [FUSE] px=%f py=%f ps=%s\n", canvas->pointer_x, canvas->pointer_y, pstate[pointer_state]); +#endif +} + +extern void +fuse_canvas_destroy(FuseCanvas canvas) +{ + free(canvas->memory); + free(canvas); +} + +extern bool +fuse_button(float x, float y, float width, float height, uint32_t selected, uint32_t hovered) +{ + size_t id = fuse__element(); + bool clicked = false; + + bool hot = FUSE_IS_HOVERED(x, y, width, height); + + if (hot) f__canvas->hot = id; + + /* are we the active component? */ + if (f__canvas->active == id) { + if (f__canvas->pointer_state == FUSE_POINTER_RELEASED) { + if (hot) clicked = true; + f__canvas->active = 0; + } + } else if (hot && f__canvas->pointer_state == FUSE_POINTER_PRESSED) { + f__canvas->active = id; + } + + FuseCmd cmd = { + .type = FUSE_CMD_RECT, + .rect = { + .color = (hot) ? hovered : selected, + .x = x, + .y = y, + .w = width, + .h = height, + } + }; + + fuse__cmdbuffer_push(cmd); + + return clicked; +} + +extern void +fuse_slider(float x, float y, float width, float height, uint32_t slider, uint32_t nob, float *nob_pos) +{ + size_t id = fuse__element(); + + /* are we the hot component? */ + bool hot = FUSE_IS_HOVERED(x, y, width, height); + + if (hot) f__canvas->hot = id; + + bool is_vert = (width < height); + float nob_width = is_vert ? width : height; + float nob_height = is_vert ? width : height; + float track_len = is_vert ? (height - nob_height) : (width - nob_width); + float p_off = is_vert ? (f__canvas->pointer_y - (y + nob_height * 0.5f)) : (f__canvas->pointer_x - (x + nob_width * 0.5f)); + float val = (track_len > 0.0f) ? (p_off / track_len) : 0.0f; + + /* calculate nob_pos from pointer */ + if (f__canvas->active == id) { + if (f__canvas->pointer_state == FUSE_POINTER_RELEASED) { + if (hot) { /* clicked */ } + f__canvas->active = 0; + } + *nob_pos = FUSE_MAX(0.0f, FUSE_MIN(1.0f, val)); + + } else if (hot && f__canvas->pointer_state == FUSE_POINTER_PRESSED) { + f__canvas->active = id; + *nob_pos = FUSE_MAX(0.0f, FUSE_MIN(1.0f, val)); + + } + + /* clamp nob_pos */ + *nob_pos = FUSE_LIMIT(*nob_pos, 0.0f, 1.0f); + + float nob_x = is_vert ? 0.0f : (*nob_pos * (width - nob_width)); + float nob_y = is_vert ? (*nob_pos * (height - nob_height)) : 0.0f; + + FuseCmd cmd = { + .type = FUSE_CMD_RECT, + .rect = { + .color = slider, + .x = x, + .y = y, + .w = width, + .h = height, + } + }; + + FuseCmd nob_rect = { + .type = FUSE_CMD_RECT, + .rect = { + .color = nob, + .x = x + nob_x, + .y = y + nob_y, + .w = nob_width, + .h = nob_height, + } + }; + + fuse__cmdbuffer_push(cmd); + fuse__cmdbuffer_push(nob_rect); +} + +extern int +fuse_tooltip(const char *text) +{ + FUSE_TODO; + /* NOTE(vasco): this is supposed to add a tooltip when the last element was hovered... + * will probably need to grab the last element's box? + * I mean i need to know it's bounds and add an element that + * shows up after being hovered over a few frames + * so we will need to retain some data via the hashmap + * + * that's the plan for now + */ + (void) text; +} + +static inline size_t +fuse__element() +{ + // we increment first because 0 is used for when no element is + // selected + size_t id = ++f__canvas->current_id; + return id; +} + +static inline void +fuse__cmdbuffer_push(FuseCmd cmd) +{ + if (f__canvas->cmdbuffer_head < f__canvas->cmdbuffer_size) + f__canvas->cmdbuffer_ptr[f__canvas->cmdbuffer_head++] = cmd; +} + +static void +fuse__cmdbuffer_print() +{ + for (size_t u = 0; u < f__canvas->cmdbuffer_head; u++) { + FuseCmd cmd = f__canvas->cmdbuffer_ptr[u]; + assert(cmd.type < FUSE_CMD_COUNT && "[FUSE] WARN: INVALID TYPE IN COMMAND BUFFER"); + + printf("CMD: %s - ", fuse__cmd_name(cmd.type)); + switch (cmd.type) { + case FUSE_CMD_RECT: + printf("x=%f y=%f w=%f h=%f col=%x", cmd.rect.x, cmd.rect.y, cmd.rect.w, cmd.rect.h, cmd.rect.color); + break; + } + printf("\n"); + } +} + +static const char* +fuse__cmd_name(uint8_t type) +{ + assert(type < FUSE_CMD_COUNT); + return fuse_cmd_name[type]; +} + + +extern bool +fuse_button_text(const char *text, float x, float y, float width, float height, uint32_t selected, uint32_t hovered) +{ + (void) text; (void) x; (void) y; (void) width; (void) height; (void) selected; (void) hovered; + FUSE_TODO; + return false; +} + +extern bool +fuse_checkbox_text(const char *text, bool *value) +{ + (void) text; (void) value; + FUSE_TODO; + return false; +} + +extern bool +fuse_element_is_hovered(const char *text) +{ + (void) text; + FUSE_TODO; + return false; +} + +extern void +fuse_hovered(void (*op)(void*), void *data) +{ + (void) op; (void) data; + FUSE_TODO; +} + +extern void +fuse_hovered_clear() +{ + FUSE_TODO; +} + +extern void +fuse_rotate_rad() +{ + FUSE_TODO; +} + +extern void +fuse_rotate_deg() +{ + FUSE_TODO; +} + +extern void +fuse_layout(FuseLayoutInfo info) +{ + (void) info; + FUSE_TODO; +} + +extern void +fuse_div_begin(float x, float y, float width, float height, uint32_t color) +{ + (void) x; (void) y; (void) width; (void) height; (void) color; + FUSE_TODO; +} + +extern void +fuse_div_end() +{ + FUSE_TODO; +} + +extern void +fuse_flex_begin(float x, float y, float width, float height, uint32_t color) +{ + (void) x; (void) y; (void) width; (void) height; (void) color; + FUSE_TODO; +} + +extern void +fuse_flex_end() +{ + FUSE_TODO; +} + +extern FuseStyle +fuse_style_save(const char *path) +{ + (void) path; + FUSE_TODO; + return NULL; +} + +extern void +fuse_style_set(FuseStyle style) +{ + (void) style; + FUSE_TODO; +} + +extern void +fuse_style_load(const char *path) +{ + (void) path; + FUSE_TODO; +} + +extern void +fuse_window_begin(char *name, float width, float height, uint32_t border_color, uint32_t border_width) +{ + (void) name; (void) width; (void) height; (void) border_color; (void) border_width; + FUSE_TODO; +} + +extern void +fuse_window_end() +{ + FUSE_TODO; +} + +extern void +fuse_grid_begin(uint32_t cols, uint32_t rows, const float *col_ratios, const float *row_ratios) +{ + (void) cols; (void) rows; (void) col_ratios; (void) row_ratios; + FUSE_TODO; +} + +extern void fuse_grid_next_row() { FUSE_TODO; } +extern void fuse_grid_next_col() { FUSE_TODO; } +extern void fuse_grid_prev_row() { FUSE_TODO; } +extern void fuse_grid_prev_col() { FUSE_TODO; } +extern void fuse_grid_next() { FUSE_TODO; } +extern void fuse_grid_prev() { FUSE_TODO; } + +extern void +fuse_grid_goto(uint32_t col, uint32_t row) +{ + (void) col; (void) row; + FUSE_TODO; +} + +extern void fuse_grid_end() { FUSE_TODO; } + +extern void fuse_3d_object() { FUSE_TODO; } +extern void fuse_3d_point_at() { FUSE_TODO; } + +extern int +fuse_animate_i(FuseAnimation anim, int i1, int i2) +{ + (void) anim; (void) i1; (void) i2; + FUSE_TODO; + return 0; +} + +extern float +fuse_animate_f(FuseAnimation anim, float f1, float f2, float f, bool loop) +{ + (void) anim; (void) f1; (void) f2; (void) f; (void) loop; + FUSE_TODO; + return 0.0f; +} + +#endif // _FUSE_C_ diff --git a/Fuse/fuse.h b/Fuse/fuse.h new file mode 100644 index 0000000..9b1540e --- /dev/null +++ b/Fuse/fuse.h @@ -0,0 +1,138 @@ +/* =========================================================================== + * FUSE - Zero Dependency UI Library - Copyright (c) 2025-2026 Vasco Alves + * See LICENCE file for license info. + * + * DESCRIPTION: + * A UI library that outputs a buffer of draw commands (or vertices) for + * you to render via a desired backend. By default all components have + * "fixed" positioning and fixed size because we assume the user has + * more knowledge of the final layout than we do. Tweening and relative + * positioning are achieved by running helper functions on the + * parameters you want to tween / be relative. + * + * EXAMPLE: + * float tween_y = fuse_tween_f(FUSE_ANIMATION_EASE_IN, 0.1, 0.3, 1, start); + * float rel_x = fuse_percent_x(30.45); + * if (fuse_button(rel_x, tween_y, 0.5, 0.3, 0xFFFF00FF, 0xFF0000FF)) { + * printf("pressed\n"); + * } + * + * =========================================================================== */ + +#ifndef _FUSE_H_ +#define _FUSE_H_ + +#define FUSE_MAJOR "0" +#define FUSE_MINOR "0" +#define FUSE_PATCH "1" + +/* CHANGE LOG + * 0.0.1 - @vasco - slider + */ + + +#include +#include +#include +#include + +#include "allocators.h" +#include "fuse_backend.h" + +/* NOTE(vasco): + * It's important that we make the API really good and future-proof. + * Functions should take as many arguments as possible. + * We can add shorthands later. + */ + +typedef struct fuse_canvas_t* FuseCanvas; // Basically the main context of each "surface" you want to draw on. +typedef struct fuse_window_t* FuseWindow; // A floating window / panel to separate your UI. +typedef struct fuse_font_t* FuseFont; +typedef struct fuse_style_t* FuseStyle; // Configurable style for your application. +typedef void(*FuseRendererCallback)(FuseCanvas, FuseCmd *cmd, void *data); +typedef void(*FuseOpCallback)(void *data); + +typedef struct FuseLayoutInfo { + float min_width, min_height; + float max_width, max_height; + float gap; + uint8_t direction; +} FuseLayoutInfo; + +typedef enum { + FUSE_ANIMATION_LINEAR = 0, + FUSE_ANIMATION_EASE_IN, + FUSE_ANIMATION_EASE_OUT +} FuseAnimation; + +typedef enum { + FUSE_POINTER_NONE = 0, + FUSE_POINTER_RELEASED, + FUSE_POINTER_PRESSED, + FUSE_POINTER_ALT +} FusePointerState; + +typedef enum { + FUSE_DIRECTION_ROW = 0, + FUSE_DIRECTION_COLUMN, +} FuseDirection; + +/* canvas creation and drawing */ +extern FuseCanvas fuse_canvas_create(size_t memory_limit, FuseStyle base_style); // Canvas initialization. Contains all the context needed to draws. Doesn't care about what you're drawing to since the library emits normalizer coordinates. +extern void fuse_canvas_set(FuseCanvas); // Changes the canvas that the rest of the API outputs to by default. Normally you will only be drawing to a single instance at a time. +extern void fuse_canvas_debug(FuseCanvas, bool); // Enable debugging for a specific canvas. +extern void fuse_canvas_clear(FuseCanvas canvas); // Clears canvas to begin drawing the next frame. +extern void fuse_canvas_draw(FuseCanvas, FuseRendererCallback, void* data); // Sends all the draw commands to the renderer callback function. +extern void fuse_canvas_resize(FuseCanvas, float w, float h); // Updates the width and height of the active canvas. +extern void fuse_canvas_pointer(FuseCanvas, int pointer_state, float x, float y); // Update pointer state on active canvas. +extern void fuse_canvas_destroy(FuseCanvas); // Clear all the context and free all the memory. + +/* components */ +extern bool fuse_button(float x, float y, float width, float height, uint32_t selected, uint32_t hovered); // Base button component. Returns true if clicked on that frame. +extern void fuse_slider(float x, float y, float width, float height, uint32_t slider, uint32_t nob, float *nob_pos); +extern bool fuse_button_text(const char *text, float x, float y, float width, float height, uint32_t selected, uint32_t hovered); +extern bool fuse_checkbox_text(const char *text, bool *value); + +/* conditionals */ +extern bool fuse_element_is_hovered(const char *text); + +/* hover callbacks */ +extern void fuse_hovered(void (*op)(void*), void *data); // Register hover callback for subsequent elements. +extern void fuse_hovered_clear(); // Clear hover callback. + + +/* operations */ +extern int fuse_tooltip(const char *text); // Adds tooltip to the NEXT element. +extern void fuse_rotate_rad(); // Rotate next element by x radians +extern void fuse_rotate_deg(); // Rotate next element by x degrees. +extern float fuse_percent_x(float p); // Get percentage of width of the parent. Left of the secreen is 0%. +extern float fuse_percent_y(float p); // Get percentage of height of the screen. Top of the screen is 0%. +extern float fuse_tween_f(FuseAnimation anim, float f1, float f2, float duration, uint64_t time_ns); // Interpolates between floats. Takes in the animation duration in seconds and the current time in nanoseconds. +extern int fuse_tween_i(FuseAnimation anim, int i1, int i2, float duration, uint64_t time_ns); // Interpolates between integers. Takes in the animation duration in seconds and the current time in nanoseconds. +extern uint32_t fuse_tween_color(FuseAnimation anim, uint32_t c1, uint32_t c2, float duration, uint64_t time_ns); // Interpolates between colors. Takes in the animation duration in seconds and the current time in nanoseconds. + +/* layouting */ +#define FUSE(...) fuse_layout((FuseLayoutInfo){ __VA_ARGS__ }) +#define FUSE_LAYOUT_DIRECTION(dir) fuse_layout((FuseLayoutInfo){ .direction = (dir) }) +extern void fuse_layout(FuseLayoutInfo); +extern void fuse_div_begin(float x, float y, float width, float height, uint32_t color); +extern void fuse_div_end(); +extern void fuse_flex_begin(float x, float y, float width, float height, uint32_t color); +extern void fuse_flex_end(); + +/* 3d */ +extern void fuse_3d_object(); +extern void fuse_3d_point_at(); + +/* do we need this? im pretty sure the user can figure it out by using a step function + * you know like the Heaviside step function u(t) where u(t) = 1, t >= 0, u(t) = 0 otherwise + * + * animation function a(t) = f(t - t0) * (u(t1) - u(t0)) + * + * in fact we already have the tween functions, all we need it u(t) and boom + */ +extern int fuse_animate_i(FuseAnimation, int i1, int i2); +extern float fuse_animate_f(FuseAnimation, float f1, float f2, float f, bool loop); + + +#endif // _FUSE_H_ diff --git a/Fuse/fuse_backend.h b/Fuse/fuse_backend.h new file mode 100644 index 0000000..57f9f80 --- /dev/null +++ b/Fuse/fuse_backend.h @@ -0,0 +1,42 @@ +#ifndef FUSE_BACKEND_H +#define FUSE_BACKEND_H + +typedef enum { + FUSE_CMD_CLIP = 0, + FUSE_CMD_RECT, + FUSE_CMD_LINE, + FUSE_CMD_COUNT +} FuseCmdType; + +typedef struct { + float x, y, w, h; +} FuseClip; + +typedef struct { + float x, y, w, h; + uint32_t color; +} FuseRect; + +typedef struct { + float x1, y1, x2, y2; + float thickness; + uint32_t color; +} FuseLine; + +typedef struct { + union { + FuseClip clip; + FuseRect rect; + FuseLine line; + }; + uint8_t type; +} FuseCmd; + + +static const char* fuse_cmd_name[] = { + [FUSE_CMD_RECT] = "RECT", + [FUSE_CMD_CLIP] = "CLIP", + [FUSE_CMD_LINE] = "LINE", +}; + +#endif diff --git a/Fuse/fuse_backend_rend.c b/Fuse/fuse_backend_rend.c new file mode 100644 index 0000000..e69de29 diff --git a/Fuse/fuse_components.c b/Fuse/fuse_components.c new file mode 100644 index 0000000..22cc394 --- /dev/null +++ b/Fuse/fuse_components.c @@ -0,0 +1,3 @@ +#pragma once +#include "fuse.h" + diff --git a/Fuse/fuse_hash.c b/Fuse/fuse_hash.c new file mode 100644 index 0000000..666915b --- /dev/null +++ b/Fuse/fuse_hash.c @@ -0,0 +1,2 @@ +#pragma once +#include "fuse_internal.h" \ No newline at end of file diff --git a/Fuse/fuse_internal.h b/Fuse/fuse_internal.h new file mode 100644 index 0000000..31fe702 --- /dev/null +++ b/Fuse/fuse_internal.h @@ -0,0 +1,63 @@ +#ifndef FUSE_INTERNAL_H +#define FUSE_INTERNAL_H + +#include "fuse.h" +#include "fuse_backend.h" + +#define FUSE_TODO \ + do { \ + fprintf(stderr, "FUSE TODO: %s() in %s:%d\n", __func__, __FILE__, __LINE__); \ + abort(); \ + } while(0) + + +typedef struct { + void *data; + size_t idk; +} FuseHash; + +struct fuse_canvas_t { + void *memory; // memory pointer to be slit into separate arenas + Arena arena; // arena the manages all our memory and will be split into smaller arenas + + FuseCmd *cmdbuffer_ptr; + + Arena element_data_arena; + FuseHash element_data; // persistent element data + + size_t cmdbuffer_head; + size_t cmdbuffer_size; + + size_t hot; + size_t active; + + size_t current_frame; + size_t current_id; + + float width; + float height; + + float pointer_x; + float pointer_y; + + uint64_t current_time_ns; + + int pointer_state; + + bool debug; +}; + +struct fuse_font_t { + int x; +}; + +struct fuse_style_t { + int x; +}; +struct fuse_window_t { + int x; +}; + +static FuseCanvas f__canvas; + +#endif diff --git a/Fuse/fuse_positioning.c b/Fuse/fuse_positioning.c new file mode 100644 index 0000000..3f22a0e --- /dev/null +++ b/Fuse/fuse_positioning.c @@ -0,0 +1,14 @@ +#pragma once +#include "fuse_internal.h" + +extern float +fuse_percent_x(float p) +{ + return p * f__canvas->width / 100; +} + +extern float +fuse_percent_y(float p) +{ + return p * f__canvas->height / 100; +} diff --git a/Fuse/fuse_test b/Fuse/fuse_test new file mode 100755 index 0000000..2188db0 Binary files /dev/null and b/Fuse/fuse_test differ diff --git a/Fuse/fuse_test.c b/Fuse/fuse_test.c new file mode 100644 index 0000000..a4b383d --- /dev/null +++ b/Fuse/fuse_test.c @@ -0,0 +1,25 @@ +#define FUSE_DEBUG +#include "fuse.h" +#include "fuse.c" + +#include "allocators.c" +#include + +float nob = 0.5; +int main() { + + FuseCanvas canvas = fuse_canvas_create(1024, NULL); /* lowk 1kb enough? */ + + fuse_canvas_clear(canvas, 400, 400, FUSE_POINTER_RELEASED, 50, 50); + + if (fuse_button(0, 0, 100, 100, 0xFF00FF, 0xFF0000)) { + printf("pressed\n"); + } + + fuse_slider(0, 0, 100, 100, 0xFF00FF, 0xFF0000, &nob); + + fuse__cmdbuffer_print(); + + fuse_canvas_destroy(canvas); + return 0; +} diff --git a/Fuse/fuse_tween.c b/Fuse/fuse_tween.c new file mode 100644 index 0000000..2b47f41 --- /dev/null +++ b/Fuse/fuse_tween.c @@ -0,0 +1,68 @@ +#pragma once + +#include "fuse.h" +#include "fuse_internal.h" + +static inline float +fuse__get_coef(FuseAnimation anim, float t) +{ + switch (anim) { + case FUSE_ANIMATION_EASE_IN: return t * t; + case FUSE_ANIMATION_EASE_OUT: return t * (2.0f - t); + case FUSE_ANIMATION_LINEAR: + default: return t; + } +} + +static inline float +fuse__ping_pong(float duration_sec, uint64_t time_ns) +{ + if (duration_sec <= 0.0f) return 1.0f; + + uint64_t duration_ns = (uint64_t)(duration_sec * 1000000000.0f); + + // modulo against DOUBLE the duration to handle reversing + uint64_t loop_time_ns = time_ns % (duration_ns * 2); + + // in the second half REVERSE the clock direction + if (loop_time_ns > duration_ns) { + loop_time_ns = (duration_ns * 2) - loop_time_ns; + } + + return (float)loop_time_ns / (float)duration_ns; +} + +extern float +fuse_tween_f(FuseAnimation anim, float f1, float f2, float duration, uint64_t time_ns) +{ + float t = fuse__ping_pong(duration, time_ns); + float coef = fuse__get_coef(anim, t); + return f1 + coef * (f2 - f1); +} + +extern int +fuse_tween_i(FuseAnimation anim, int i1, int i2, float duration, uint64_t time_ns) +{ + float t = fuse__ping_pong(duration, time_ns); + float coef = fuse__get_coef(anim, t); + return (int)(i1 + coef * (i2 - i1)); +} + +extern uint32_t +fuse_tween_color(FuseAnimation anim, uint32_t c1, uint32_t c2, float duration, uint64_t time_ns) +{ + float t = fuse__ping_pong(duration, time_ns); + float coef = fuse__get_coef(anim, t); + + int r1 = (c1 >> 24) & 0xFF, r2 = (c2 >> 24) & 0xFF; + int g1 = (c1 >> 16) & 0xFF, g2 = (c2 >> 16) & 0xFF; + int b1 = (c1 >> 8) & 0xFF, b2 = (c2 >> 8) & 0xFF; + int a1 = c1 & 0xFF, a2 = c2 & 0xFF; + + uint32_t r = (uint32_t)(r1 + coef * (r2 - r1)); + uint32_t g = (uint32_t)(g1 + coef * (g2 - g1)); + uint32_t b = (uint32_t)(b1 + coef * (b2 - b1)); + uint32_t a = (uint32_t)(a1 + coef * (a2 - a1)); + + return (r << 24) | (g << 16) | (b << 8) | a; +} -- cgit v1.3.1