summaryrefslogtreecommitdiff
path: root/Fuse
diff options
context:
space:
mode:
Diffstat (limited to 'Fuse')
-rw-r--r--Fuse/allocators.c303
-rw-r--r--Fuse/allocators.h60
-rw-r--r--Fuse/demos/fuse.slang31
-rw-r--r--Fuse/demos/fuse_demo.c280
-rw-r--r--Fuse/fuse.c436
-rw-r--r--Fuse/fuse.h138
-rw-r--r--Fuse/fuse_backend.h42
-rw-r--r--Fuse/fuse_backend_rend.c0
-rw-r--r--Fuse/fuse_components.c3
-rw-r--r--Fuse/fuse_hash.c2
-rw-r--r--Fuse/fuse_internal.h63
-rw-r--r--Fuse/fuse_positioning.c14
-rwxr-xr-xFuse/fuse_testbin0 -> 35776 bytes
-rw-r--r--Fuse/fuse_test.c25
-rw-r--r--Fuse/fuse_tween.c68
15 files changed, 1465 insertions, 0 deletions
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 <stddef.h>
+#include <stdint.h>
+#include <assert.h>
+#include <string.h>
+
+#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> 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 <stddef.h>
+#include <stdio.h>
+
+/* 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 <stdint.h>
+#include <string.h>
+
+#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 <stdint.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#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
--- /dev/null
+++ b/Fuse/fuse_backend_rend.c
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
--- /dev/null
+++ b/Fuse/fuse_test
Binary files 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 <stdio.h>
+
+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;
+}