Mercurial
view infinite_canvas/canvas.c @ 276:b55c22cff335
Add interactive infinite canvas prototype
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 17 Aug 2026 16:57:56 -0700 |
| parents | |
| children | 8d560f50ed4c |
line wrap: on
line source
#include "infinite_canvas/canvas.h" #include <math.h> #include <stdarg.h> #include <stdio.h> #include <string.h> static const char *CANVAS_DROPDOWN_OPTIONS[] = { "Design", "Engineering", "Research", }; static const char *CANVAS_TABLE_NAMES[] = { "Canvas", "Browser", "Notes", "Assets", }; static const char *CANVAS_TABLE_STATUSES[] = { "Ready", "Live", "Draft", "Synced", }; static float Canvas_Clamp(float value, float min_value, float max_value) { if (value < min_value) return min_value; if (value > max_value) return max_value; return value; } static Color Canvas_Entity_Color(uint32 index) { static const Color colors[] = { {96, 165, 250, 255}, {251, 191, 36, 255}, {167, 139, 250, 255}, {45, 212, 191, 255}, {251, 113, 133, 255}, }; return colors[index % (sizeof(colors) / sizeof(colors[0]))]; } static Color Canvas_Color_Lerp(Color from, Color to, float amount) { amount = Canvas_Clamp(amount, 0.0f, 1.0f); return (Color){ (uint8)((float)from.r + ((float)to.r - (float)from.r) * amount), (uint8)((float)from.g + ((float)to.g - (float)from.g) * amount), (uint8)((float)from.b + ((float)to.b - (float)from.b) * amount), (uint8)((float)from.a + ((float)to.a - (float)from.a) * amount), }; } void Canvas_Camera_Init(Canvas_Camera *p_camera, int32 width, int32 height) { *p_camera = (Canvas_Camera){ .target = {0.0f, 0.0f}, .zoom = 1.0f, .viewport_width = width, .viewport_height = height, .min_zoom = 0.01f, .max_zoom = 64.0f, }; } void Canvas_Camera_Set_Viewport(Canvas_Camera *p_camera, int32 width, int32 height) { p_camera->viewport_width = width; p_camera->viewport_height = height; } Vector2 Canvas_Camera_Screen_To_World(const Canvas_Camera *p_camera, Vector2 screen) { Vector2 center = { (float)p_camera->viewport_width * 0.5f, (float)p_camera->viewport_height * 0.5f, }; return (Vector2){ p_camera->target.x + (screen.x - center.x) / p_camera->zoom, p_camera->target.y + (screen.y - center.y) / p_camera->zoom, }; } Vector2 Canvas_Camera_World_To_Screen(const Canvas_Camera *p_camera, Vector2 world) { Vector2 center = { (float)p_camera->viewport_width * 0.5f, (float)p_camera->viewport_height * 0.5f, }; return (Vector2){ center.x + (world.x - p_camera->target.x) * p_camera->zoom, center.y + (world.y - p_camera->target.y) * p_camera->zoom, }; } void Canvas_Camera_Pan(Canvas_Camera *p_camera, Vector2 screen_delta) { p_camera->target.x -= screen_delta.x / p_camera->zoom; p_camera->target.y -= screen_delta.y / p_camera->zoom; } void Canvas_Camera_Zoom_At( Canvas_Camera *p_camera, Vector2 screen_anchor, float zoom_multiplier) { Vector2 world_anchor = Canvas_Camera_Screen_To_World(p_camera, screen_anchor); p_camera->zoom = Canvas_Clamp( p_camera->zoom * zoom_multiplier, p_camera->min_zoom, p_camera->max_zoom); Vector2 moved_anchor = Canvas_Camera_Screen_To_World(p_camera, screen_anchor); p_camera->target.x += world_anchor.x - moved_anchor.x; p_camera->target.y += world_anchor.y - moved_anchor.y; } Rectangle Canvas_Camera_Viewport_Bounds(const Canvas_Camera *p_camera) { Vector2 min = Canvas_Camera_Screen_To_World(p_camera, (Vector2){0.0f, 0.0f}); Vector2 max = Canvas_Camera_Screen_To_World( p_camera, (Vector2){(float)p_camera->viewport_width, (float)p_camera->viewport_height}); return (Rectangle){ .x = min.x, .y = min.y, .width = max.x - min.x, .height = max.y - min.y, }; } static Rectangle Canvas_Entity_Bounds(const Canvas_Entity *p_entity) { if (p_entity->type == CANVAS_ENTITY_CIRCLE) { return (Rectangle){ p_entity->position.x - p_entity->size.x, p_entity->position.y - p_entity->size.x, p_entity->size.x * 2.0f, p_entity->size.x * 2.0f, }; } if (p_entity->type == CANVAS_ENTITY_LINE) { float end_x = p_entity->position.x + p_entity->size.x; float end_y = p_entity->position.y + p_entity->size.y; return (Rectangle){ fminf(p_entity->position.x, end_x), fminf(p_entity->position.y, end_y), fmaxf(fabsf(p_entity->size.x), 0.001f), fmaxf(fabsf(p_entity->size.y), 0.001f), }; } Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; if (p_entity->type == CANVAS_ENTITY_DROPDOWN && p_entity->active) { bounds.height += 128.0f; } return bounds; } typedef struct { char *p_buffer; size_t capacity; size_t length; boolean truncated; } Canvas_Context_Writer; static void Canvas_Context_Append( Canvas_Context_Writer *p_writer, const char *p_format, ...) { if (p_writer->truncated || p_writer->capacity == 0) { p_writer->truncated = TRUE; return; } va_list arguments; va_start(arguments, p_format); int32 written = vsnprintf( p_writer->p_buffer + p_writer->length, p_writer->capacity - p_writer->length, p_format, arguments); va_end(arguments); if (written < 0) { p_writer->truncated = TRUE; return; } size_t available = p_writer->capacity - p_writer->length; if ((size_t)written >= available) { p_writer->length = p_writer->capacity - 1; p_writer->truncated = TRUE; return; } p_writer->length += (size_t)written; } static void Canvas_Context_Append_Quoted( Canvas_Context_Writer *p_writer, const char *p_text) { Canvas_Context_Append(p_writer, "\""); for (const char *p_cursor = p_text; *p_cursor && !p_writer->truncated; p_cursor++) { if (*p_cursor == '"' || *p_cursor == '\\') { Canvas_Context_Append(p_writer, "\\%c", *p_cursor); } else if (*p_cursor == '\n') { Canvas_Context_Append(p_writer, "\\n"); } else if (*p_cursor == '\r') { continue; } else { Canvas_Context_Append(p_writer, "%c", *p_cursor); } } Canvas_Context_Append(p_writer, "\""); } static void Canvas_Context_Append_Entity_State( Canvas_Context_Writer *p_writer, const Canvas_Entity *p_entity) { switch (p_entity->type) { case CANVAS_ENTITY_RECTANGLE: case CANVAS_ENTITY_CIRCLE: case CANVAS_ENTITY_LINE: Canvas_Context_Append( p_writer, "color=rgba(%u,%u,%u,%u)", p_entity->color.r, p_entity->color.g, p_entity->color.b, p_entity->color.a); break; case CANVAS_ENTITY_TEXT: Canvas_Context_Append(p_writer, "content="); Canvas_Context_Append_Quoted(p_writer, p_entity->text); break; case CANVAS_ENTITY_BUTTON: Canvas_Context_Append(p_writer, "label="); Canvas_Context_Append_Quoted(p_writer, p_entity->text); Canvas_Context_Append( p_writer, " activated=%s", p_entity->active ? "true" : "false"); break; case CANVAS_ENTITY_TEXT_AREA: Canvas_Context_Append(p_writer, "content="); Canvas_Context_Append_Quoted(p_writer, p_entity->text); Canvas_Context_Append( p_writer, " editing=%s", p_entity->active ? "true" : "false"); break; case CANVAS_ENTITY_DROPDOWN: Canvas_Context_Append(p_writer, "selected="); Canvas_Context_Append_Quoted( p_writer, CANVAS_DROPDOWN_OPTIONS[p_entity->value]); Canvas_Context_Append( p_writer, " open=%s", p_entity->active ? "true" : "false"); break; case CANVAS_ENTITY_ACCORDION: Canvas_Context_Append(p_writer, "title="); Canvas_Context_Append_Quoted(p_writer, p_entity->text); Canvas_Context_Append( p_writer, " expanded=%s", p_entity->active ? "true" : "false"); break; case CANVAS_ENTITY_CARD: Canvas_Context_Append(p_writer, "title="); Canvas_Context_Append_Quoted(p_writer, p_entity->text); Canvas_Context_Append( p_writer, " emphasized=%s", p_entity->active ? "true" : "false"); break; case CANVAS_ENTITY_CALENDAR: Canvas_Context_Append( p_writer, "selected_date=\"2026-08-%02d\"", p_entity->value); break; case CANVAS_ENTITY_SWITCH: Canvas_Context_Append( p_writer, "checked=%s", p_entity->active ? "true" : "false"); break; case CANVAS_ENTITY_TABLE: Canvas_Context_Append(p_writer, "selected_row={name="); Canvas_Context_Append_Quoted( p_writer, CANVAS_TABLE_NAMES[p_entity->value]); Canvas_Context_Append(p_writer, ", status="); Canvas_Context_Append_Quoted( p_writer, CANVAS_TABLE_STATUSES[p_entity->value]); Canvas_Context_Append(p_writer, "}"); break; case CANVAS_ENTITY_NOTIFICATION: Canvas_Context_Append(p_writer, "title="); Canvas_Context_Append_Quoted(p_writer, p_entity->text); Canvas_Context_Append( p_writer, " active=%s", p_entity->active ? "true" : "false"); break; case CANVAS_ENTITY_SCROLL_AREA: { int32 first_item = p_entity->value / 42 + 1; int32 last_item = first_item + 3; if (last_item > 10) last_item = 10; Canvas_Context_Append( p_writer, "scroll_offset=%d visible_items=%d-%d", p_entity->value, first_item, last_item); break; } case CANVAS_ENTITY_IMAGE: Canvas_Context_Append(p_writer, "source="); Canvas_Context_Append_Quoted(p_writer, p_entity->text); break; case CANVAS_ENTITY_WEB_CONTENT: Canvas_Context_Append(p_writer, "url="); Canvas_Context_Append_Quoted(p_writer, p_entity->text); break; default: Canvas_Context_Append(p_writer, "value=%d", p_entity->value); break; } } Canvas_Context_Snapshot Canvas_Scene_Build_Visible_Context( const Canvas_Scene *p_scene, const Canvas_Camera *p_camera, char *p_buffer, size_t buffer_size) { Canvas_Context_Writer writer = { .p_buffer = p_buffer, .capacity = buffer_size, }; if (buffer_size > 0) p_buffer[0] = '\0'; Rectangle viewport = Canvas_Camera_Viewport_Bounds(p_camera); Canvas_Context_Append( &writer, "camera_context zoom=%.2f viewport=[%.1f, %.1f, %.1f, %.1f]\n" "entities:\n", p_camera->zoom, viewport.x, viewport.y, viewport.width, viewport.height); Canvas_Context_Snapshot snapshot = {0}; for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { const Canvas_Entity *p_entity = &p_scene->p_entities[index]; if (p_entity->type == CANVAS_ENTITY_NOTIFICATION && p_entity->animation_amount <= 0.01f) { continue; } if (!CheckCollisionRecs(viewport, Canvas_Entity_Bounds(p_entity))) continue; snapshot.visible_count++; Canvas_Context_Append( &writer, "- id=%u type=\"%s\" position=[%.1f, %.1f] size=[%.1f, %.1f] pinned=%s\n" " state: ", p_entity->id, Canvas_Entity_Type_Name(p_entity->type), p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, p_entity->pinned ? "true" : "false"); Canvas_Context_Append_Entity_State(&writer, p_entity); Canvas_Context_Append(&writer, "\n"); } if (snapshot.visible_count == 0) { Canvas_Context_Append(&writer, " (none)\n"); } snapshot.bytes_written = writer.length; snapshot.truncated = writer.truncated; return snapshot; } float Canvas_Grid_Spacing(float zoom) { float spacing = 64.0f; while ((spacing * zoom < 48.0f) && (spacing < 1048576.0f)) spacing *= 2.0f; while ((spacing * zoom > 96.0f) && (spacing > 0.0009765625f)) spacing *= 0.5f; return spacing; } void Canvas_Scene_Init(Canvas_Scene *p_scene, Dowa_Arena *p_arena) { memset(p_scene, 0, sizeof(*p_scene)); p_scene->p_arena = p_arena; p_scene->selected_index = -1; p_scene->pressed_index = -1; p_scene->hover_index = -1; p_scene->show_grid = FALSE; Dowa_Array_Reserve_Arena(p_scene->p_entities, CANVAS_MAX_ENTITIES, p_arena); } boolean Canvas_Scene_Add(Canvas_Scene *p_scene, Canvas_Entity_Type type, Vector2 position) { if (Dowa_Array_Length(p_scene->p_entities) >= CANVAS_MAX_ENTITIES) return FALSE; uint32 index = p_scene->next_entity++; Canvas_Entity entity = { .id = ++p_scene->next_entity_id, .type = type, .position = position, .size = {160.0f, 100.0f}, .color = Canvas_Entity_Color(index), .text = {0}, .value = 0, .active = FALSE, .pinned = FALSE, .hover_amount = 0.0f, .animation_amount = 0.0f, }; if (type == CANVAS_ENTITY_CIRCLE) entity.size = (Vector2){56.0f, 56.0f}; if (type == CANVAS_ENTITY_LINE) entity.size = (Vector2){180.0f, 80.0f}; if (type == CANVAS_ENTITY_TEXT) { entity.size = (Vector2){180.0f, 32.0f}; snprintf(entity.text, sizeof(entity.text), "Entity %u", index + 1); } if (type == CANVAS_ENTITY_BUTTON) { entity.size = (Vector2){156.0f, 48.0f}; snprintf(entity.text, sizeof(entity.text), "Continue"); } if (type == CANVAS_ENTITY_TEXT_AREA) { entity.size = (Vector2){280.0f, 148.0f}; } if (type == CANVAS_ENTITY_DROPDOWN) { entity.size = (Vector2){224.0f, 48.0f}; } if (type == CANVAS_ENTITY_ACCORDION) { entity.size = (Vector2){300.0f, 56.0f}; snprintf(entity.text, sizeof(entity.text), "How does the canvas work?"); } if (type == CANVAS_ENTITY_CARD) { entity.size = (Vector2){280.0f, 220.0f}; snprintf(entity.text, sizeof(entity.text), "Infinite workspace"); } if (type == CANVAS_ENTITY_CALENDAR) { entity.size = (Vector2){280.0f, 300.0f}; entity.value = 17; } if (type == CANVAS_ENTITY_SWITCH) { entity.size = (Vector2){64.0f, 36.0f}; } if (type == CANVAS_ENTITY_TABLE) { entity.size = (Vector2){420.0f, 230.0f}; entity.value = 1; } if (type == CANVAS_ENTITY_NOTIFICATION) { entity.size = (Vector2){384.0f, 82.0f}; entity.active = TRUE; entity.animation_amount = 1.0f; snprintf(entity.text, sizeof(entity.text), "Event created"); } if (type == CANVAS_ENTITY_SCROLL_AREA) { entity.size = (Vector2){320.0f, 220.0f}; } if (type == CANVAS_ENTITY_IMAGE) { entity.size = (Vector2){360.0f, 240.0f}; snprintf( entity.text, sizeof(entity.text), "infinite_canvas/assets/image-placeholder.svg"); } if (type == CANVAS_ENTITY_WEB_CONTENT) { entity.size = (Vector2){720.0f, 460.0f}; entity.active = TRUE; snprintf(entity.text, sizeof(entity.text), "https://mrjunejune.com"); } Dowa_Array_Push_Arena(p_scene->p_entities, entity, p_scene->p_arena); return TRUE; } void Canvas_Scene_Clear(Canvas_Scene *p_scene) { Dowa_Array_Clear(p_scene->p_entities); p_scene->next_entity = 0; p_scene->selected_index = -1; p_scene->pressed_index = -1; p_scene->hover_index = -1; p_scene->dragging = FALSE; p_scene->resizing = FALSE; p_scene->notification_stack_amount = 0.0f; } void Canvas_Scene_Add_Demo(Canvas_Scene *p_scene) { Canvas_Scene_Clear(p_scene); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_RECTANGLE, (Vector2){-550.0f, -520.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_CIRCLE, (Vector2){-300.0f, -470.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_LINE, (Vector2){-180.0f, -500.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_TEXT, (Vector2){50.0f, -500.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_BUTTON, (Vector2){280.0f, -510.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_SWITCH, (Vector2){500.0f, -504.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_TEXT_AREA, (Vector2){-550.0f, -350.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_DROPDOWN, (Vector2){-230.0f, -350.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_ACCORDION, (Vector2){30.0f, -350.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_CARD, (Vector2){360.0f, -350.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_CALENDAR, (Vector2){-550.0f, -130.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_TABLE, (Vector2){-230.0f, -130.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_NOTIFICATION, (Vector2){220.0f, -130.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_NOTIFICATION, (Vector2){220.0f, -20.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_NOTIFICATION, (Vector2){220.0f, 90.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_SCROLL_AREA, (Vector2){610.0f, -130.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_IMAGE, (Vector2){-550.0f, 220.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_WEB_CONTENT, (Vector2){-150.0f, 120.0f}); } void Canvas_Scene_Add_Browser_Grid(Canvas_Scene *p_scene) { Canvas_Scene_Clear(p_scene); for (int32 row = 0; row < 2; row++) { for (int32 column = 0; column < 5; column++) { Vector2 position = { -614.0f + (float)column * 248.0f, -182.0f + (float)row * 188.0f, }; if (!Canvas_Scene_Add(p_scene, CANVAS_ENTITY_WEB_CONTENT, position)) return; Canvas_Entity *p_entity = &p_scene->p_entities[Dowa_Array_Length(p_scene->p_entities) - 1]; p_entity->size = (Vector2){236.0f, 176.0f}; } } } static float Canvas_Distance_To_Segment(Vector2 point, Vector2 start, Vector2 end) { Vector2 segment = {end.x - start.x, end.y - start.y}; Vector2 relative = {point.x - start.x, point.y - start.y}; float length_squared = segment.x * segment.x + segment.y * segment.y; if (length_squared <= 0.0001f) { return sqrtf(relative.x * relative.x + relative.y * relative.y); } float projection = (relative.x * segment.x + relative.y * segment.y) / length_squared; projection = Canvas_Clamp(projection, 0.0f, 1.0f); Vector2 closest = { start.x + segment.x * projection, start.y + segment.y * projection, }; float dx = point.x - closest.x; float dy = point.y - closest.y; return sqrtf(dx * dx + dy * dy); } static boolean Canvas_Entity_Contains( const Canvas_Entity *p_entity, Vector2 point, float zoom) { switch (p_entity->type) { case CANVAS_ENTITY_RECTANGLE: return CheckCollisionPointRec( point, (Rectangle){ p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }) ? TRUE : FALSE; case CANVAS_ENTITY_CIRCLE: { float dx = point.x - p_entity->position.x; float dy = point.y - p_entity->position.y; return (dx * dx + dy * dy <= p_entity->size.x * p_entity->size.x) ? TRUE : FALSE; } case CANVAS_ENTITY_LINE: { Vector2 end = { p_entity->position.x + p_entity->size.x, p_entity->position.y + p_entity->size.y, }; return Canvas_Distance_To_Segment(point, p_entity->position, end) <= 10.0f / zoom; } case CANVAS_ENTITY_TEXT: case CANVAS_ENTITY_BUTTON: case CANVAS_ENTITY_TEXT_AREA: case CANVAS_ENTITY_DROPDOWN: case CANVAS_ENTITY_ACCORDION: case CANVAS_ENTITY_CARD: case CANVAS_ENTITY_CALENDAR: case CANVAS_ENTITY_SWITCH: case CANVAS_ENTITY_TABLE: case CANVAS_ENTITY_SCROLL_AREA: case CANVAS_ENTITY_IMAGE: { float height = p_entity->size.y; if (p_entity->type == CANVAS_ENTITY_DROPDOWN && p_entity->active) { height += 8.0f + 3.0f * 40.0f; } return CheckCollisionPointRec( point, (Rectangle){ p_entity->position.x, p_entity->position.y, p_entity->size.x, height, }) ? TRUE : FALSE; } case CANVAS_ENTITY_NOTIFICATION: if (p_entity->animation_amount <= 0.01f) return FALSE; return CheckCollisionPointRec( point, (Rectangle){ p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }) ? TRUE : FALSE; case CANVAS_ENTITY_WEB_CONTENT: { float frame_inset = CANVAS_WEB_FRAME_INSET / zoom; float resize_handle = CANVAS_WEB_RESIZE_HANDLE / zoom; Rectangle outer = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Rectangle content = { outer.x + frame_inset, outer.y + frame_inset, outer.width - frame_inset - resize_handle, outer.height - frame_inset - resize_handle, }; return CheckCollisionPointRec(point, outer) && !CheckCollisionPointRec(point, content); } default: return FALSE; } } static boolean Canvas_Web_Resize_Handle_Contains( const Canvas_Entity *p_entity, Vector2 point, float zoom) { if (p_entity->type != CANVAS_ENTITY_WEB_CONTENT && p_entity->type != CANVAS_ENTITY_IMAGE) { return FALSE; } float handle_size = CANVAS_WEB_RESIZE_HANDLE / zoom; return CheckCollisionPointRec( point, (Rectangle){ p_entity->position.x + p_entity->size.x - handle_size, p_entity->position.y + p_entity->size.y - handle_size, handle_size, handle_size, }) ? TRUE : FALSE; } int32 Canvas_Scene_Pick( const Canvas_Scene *p_scene, Vector2 world_pointer, float zoom) { for (int32 index = (int32)Dowa_Array_Length(p_scene->p_entities) - 1; index >= 0; index--) { const Canvas_Entity *p_entity = &p_scene->p_entities[index]; if (p_entity->type == CANVAS_ENTITY_DROPDOWN && p_entity->active && Canvas_Entity_Contains(p_entity, world_pointer, zoom)) { return index; } } for (int32 index = (int32)Dowa_Array_Length(p_scene->p_entities) - 1; index >= 0; index--) { if (Canvas_Entity_Contains(&p_scene->p_entities[index], world_pointer, zoom)) return index; } return -1; } static void Canvas_Scene_Activate(Canvas_Scene *p_scene, int32 index, Vector2 world_pointer) { if (index < 0 || index >= (int32)Dowa_Array_Length(p_scene->p_entities)) return; Canvas_Entity *p_entity = &p_scene->p_entities[index]; switch (p_entity->type) { case CANVAS_ENTITY_BUTTON: p_entity->active = !p_entity->active; break; case CANVAS_ENTITY_TEXT_AREA: p_entity->active = TRUE; break; case CANVAS_ENTITY_DROPDOWN: if (world_pointer.y <= p_entity->position.y + p_entity->size.y) { p_entity->active = !p_entity->active; } else if (p_entity->active) { float options_y = p_entity->position.y + p_entity->size.y + 8.0f; int32 option = (int32)((world_pointer.y - options_y) / 40.0f); if (option >= 0 && option < 3) p_entity->value = option; p_entity->active = FALSE; } break; case CANVAS_ENTITY_ACCORDION: p_entity->active = !p_entity->active; break; case CANVAS_ENTITY_CARD: case CANVAS_ENTITY_SWITCH: p_entity->active = !p_entity->active; break; case CANVAS_ENTITY_NOTIFICATION: { float local_x = (world_pointer.x - p_entity->position.x) / p_entity->size.x; if (local_x >= 0.68f) p_entity->active = FALSE; break; } case CANVAS_ENTITY_CALENDAR: { float grid_x = p_entity->position.x + 14.0f; float grid_y = p_entity->position.y + 82.0f; if (world_pointer.x < grid_x || world_pointer.y < grid_y || world_pointer.x >= grid_x + 252.0f || world_pointer.y >= grid_y + 192.0f) { break; } int32 column = (int32)((world_pointer.x - grid_x) / 36.0f); int32 row = (int32)((world_pointer.y - grid_y) / 32.0f); int32 day = row * 7 + column - 6 + 1; if (column >= 0 && column < 7 && row >= 0 && row < 6 && day >= 1 && day <= 31) { p_entity->value = day; } break; } case CANVAS_ENTITY_TABLE: { float rows_y = p_entity->position.y + 62.0f; int32 row = (int32)((world_pointer.y - rows_y) / 38.0f); if (world_pointer.y >= rows_y && row >= 0 && row < 4) { p_entity->value = row; } break; } default: break; } } static void Canvas_Scene_Update_Text_Input(Canvas_Scene *p_scene) { if (p_scene->selected_index < 0 || p_scene->selected_index >= (int32)Dowa_Array_Length(p_scene->p_entities)) return; Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index]; if (p_entity->type != CANVAS_ENTITY_TEXT_AREA || !p_entity->active) return; size_t length = strlen(p_entity->text); int32 codepoint = GetCharPressed(); while (codepoint > 0) { if (codepoint >= 32 && codepoint <= 126 && length + 1 < sizeof(p_entity->text)) { p_entity->text[length++] = (char)codepoint; p_entity->text[length] = '\0'; } codepoint = GetCharPressed(); } if (IsKeyPressed(KEY_ENTER) && length + 1 < sizeof(p_entity->text)) { p_entity->text[length++] = '\n'; p_entity->text[length] = '\0'; } if (IsKeyPressed(KEY_BACKSPACE) && length > 0) { p_entity->text[length - 1] = '\0'; } } static void Canvas_Entity_Capture_Pin( Canvas_Entity *p_entity, const Canvas_Camera *p_camera) { p_entity->pinned_screen_position = Canvas_Camera_World_To_Screen(p_camera, p_entity->position); p_entity->pinned_screen_size = (Vector2){ p_entity->size.x * p_camera->zoom, p_entity->size.y * p_camera->zoom, }; } static void Canvas_Scene_Update_Animations( Canvas_Scene *p_scene, const Canvas_Camera *p_camera) { for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { Canvas_Entity *p_entity = &p_scene->p_entities[index]; if (p_entity->type != CANVAS_ENTITY_ACCORDION && p_entity->type != CANVAS_ENTITY_SWITCH) { continue; } float target = p_entity->active ? 1.0f : 0.0f; float speed = p_entity->type == CANVAS_ENTITY_SWITCH ? 7.0f : 4.5f; float step = GetFrameTime() * speed; if (p_entity->animation_amount < target) { p_entity->animation_amount = fminf( target, p_entity->animation_amount + step); } else if (p_entity->animation_amount > target) { p_entity->animation_amount = fmaxf( target, p_entity->animation_amount - step); } float amount = p_entity->animation_amount; float eased = amount * amount * (3.0f - 2.0f * amount); if (p_entity->type == CANVAS_ENTITY_SWITCH) continue; if (p_entity->pinned) { float pinned_scale = p_entity->pinned_screen_size.x / 300.0f; p_entity->pinned_screen_size.y = (56.0f + 108.0f * eased) * pinned_scale; p_entity->size.y = p_entity->pinned_screen_size.y / p_camera->zoom; } else { p_entity->size.y = 56.0f + 108.0f * eased; } } } boolean Canvas_Scene_Update_Interaction( Canvas_Scene *p_scene, const Canvas_Camera *p_camera, boolean block_pointer_input) { Canvas_Scene_Update_Text_Input(p_scene); Canvas_Scene_Update_Animations(p_scene, p_camera); if (block_pointer_input && p_scene->pressed_index < 0) { return FALSE; } Vector2 world_pointer = Canvas_Camera_Screen_To_World(p_camera, GetMousePosition()); p_scene->hover_index = block_pointer_input ? -1 : Canvas_Scene_Pick(p_scene, world_pointer, p_camera->zoom); boolean consumed_scroll = FALSE; if (p_scene->hover_index >= 0) { Canvas_Entity *p_hovered = &p_scene->p_entities[p_scene->hover_index]; if (p_hovered->type == CANVAS_ENTITY_SCROLL_AREA) { float wheel = GetMouseWheelMove(); if (wheel != 0.0f) { p_hovered->value = (int32)Canvas_Clamp( (float)p_hovered->value - wheel * 28.0f, 0.0f, 240.0f); consumed_scroll = TRUE; } } } float animation_step = Canvas_Clamp(GetFrameTime() * 12.0f, 0.0f, 1.0f); for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { float target = ((int32)index == p_scene->hover_index) ? 1.0f : 0.0f; Canvas_Entity *p_entity = &p_scene->p_entities[index]; p_entity->hover_amount += (target - p_entity->hover_amount) * animation_step; } if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && !IsKeyDown(KEY_SPACE)) { p_scene->selected_index = Canvas_Scene_Pick(p_scene, world_pointer, p_camera->zoom); for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { if ((int32)index != p_scene->selected_index && p_scene->p_entities[index].type == CANVAS_ENTITY_TEXT_AREA) { p_scene->p_entities[index].active = FALSE; } if ((int32)index != p_scene->selected_index && p_scene->p_entities[index].type == CANVAS_ENTITY_DROPDOWN) { p_scene->p_entities[index].active = FALSE; } } if (p_scene->selected_index >= 0) { Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index]; p_scene->pressed_index = p_scene->selected_index; p_scene->dragging = FALSE; p_scene->resizing = Canvas_Web_Resize_Handle_Contains( p_entity, world_pointer, p_camera->zoom); p_scene->pointer_start = world_pointer; p_scene->entity_start = p_entity->position; p_scene->entity_size_start = p_entity->size; } } if (p_scene->pressed_index >= 0 && IsMouseButtonDown(MOUSE_BUTTON_LEFT)) { Vector2 delta = { world_pointer.x - p_scene->pointer_start.x, world_pointer.y - p_scene->pointer_start.y, }; float screen_distance_squared = (delta.x * p_camera->zoom) * (delta.x * p_camera->zoom) + (delta.y * p_camera->zoom) * (delta.y * p_camera->zoom); if (screen_distance_squared >= 16.0f) p_scene->dragging = TRUE; if (p_scene->dragging) { Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->pressed_index]; if (p_entity->type == CANVAS_ENTITY_NOTIFICATION) { p_scene->dragging = FALSE; } else if (p_scene->resizing) { p_entity->size = (Vector2){ fmaxf(CANVAS_WEB_MIN_WIDTH, p_scene->entity_size_start.x + delta.x), fmaxf(CANVAS_WEB_MIN_HEIGHT, p_scene->entity_size_start.y + delta.y), }; } else { p_entity->position = (Vector2){ p_scene->entity_start.x + delta.x, p_scene->entity_start.y + delta.y, }; } if (p_entity->pinned) Canvas_Entity_Capture_Pin(p_entity, p_camera); } } if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) { if (p_scene->pressed_index >= 0 && !p_scene->dragging) { Canvas_Scene_Activate(p_scene, p_scene->pressed_index, world_pointer); Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->pressed_index]; if (p_entity->pinned) Canvas_Entity_Capture_Pin(p_entity, p_camera); } p_scene->pressed_index = -1; p_scene->dragging = FALSE; p_scene->resizing = FALSE; } return p_scene->pressed_index >= 0 || consumed_scroll; } boolean Canvas_Scene_Is_Text_Editing(const Canvas_Scene *p_scene) { if (p_scene->selected_index < 0 || p_scene->selected_index >= (int32)Dowa_Array_Length(p_scene->p_entities)) { return FALSE; } const Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index]; return (p_entity->type == CANVAS_ENTITY_TEXT_AREA && p_entity->active) ? TRUE : FALSE; } void Canvas_Scene_Toggle_Selected_Pin( Canvas_Scene *p_scene, const Canvas_Camera *p_camera) { if (p_scene->selected_index < 0 || p_scene->selected_index >= (int32)Dowa_Array_Length(p_scene->p_entities)) { return; } Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index]; if (p_entity->type == CANVAS_ENTITY_NOTIFICATION) return; p_entity->pinned = !p_entity->pinned; if (p_entity->pinned) Canvas_Entity_Capture_Pin(p_entity, p_camera); } void Canvas_Scene_Update_Notification_Stack( Canvas_Scene *p_scene, const Canvas_Camera *p_camera) { const float width = 384.0f; const float height = 82.0f; const float margin = 24.0f; const float gap = 12.0f; float delta = GetFrameTime(); float follow = 1.0f - expf(-delta * 16.0f); Vector2 mouse = GetMousePosition(); boolean hovered = FALSE; int32 active_count = 0; for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { Canvas_Entity *p_entity = &p_scene->p_entities[index]; if (p_entity->type != CANVAS_ENTITY_NOTIFICATION) continue; float visibility_target = p_entity->active ? 1.0f : 0.0f; if (p_entity->animation_amount < visibility_target) { p_entity->animation_amount = fminf( visibility_target, p_entity->animation_amount + delta * 7.0f); } else if (p_entity->animation_amount > visibility_target) { p_entity->animation_amount = fmaxf( visibility_target, p_entity->animation_amount - delta * 7.0f); } if (p_entity->active) active_count++; if (p_entity->screen_space_initialized && p_entity->animation_amount > 0.01f && CheckCollisionPointRec( mouse, (Rectangle){ p_entity->pinned_screen_position.x, p_entity->pinned_screen_position.y, p_entity->pinned_screen_size.x, p_entity->pinned_screen_size.y, })) { hovered = TRUE; } } if (!hovered && active_count > 1 && p_scene->notification_stack_amount > 0.08f) { float right = (float)p_camera->viewport_width - margin; float bottom = (float)p_camera->viewport_height - margin; float top = bottom - (float)active_count * height - (float)(active_count - 1) * gap; hovered = CheckCollisionPointRec( mouse, (Rectangle){right - width, top, width, bottom - top}) ? TRUE : FALSE; } float stack_target = hovered ? 1.0f : 0.0f; p_scene->notification_stack_amount += (stack_target - p_scene->notification_stack_amount) * follow; float amount = p_scene->notification_stack_amount; float eased = amount * amount * (3.0f - 2.0f * amount); int32 rank = 0; for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { Canvas_Entity *p_entity = &p_scene->p_entities[index]; if (p_entity->type != CANVAS_ENTITY_NOTIFICATION || p_entity->animation_amount <= 0.01f) { continue; } Vector2 target_position = p_entity->pinned_screen_position; Vector2 target_size = {width, height}; if (p_entity->active) { int32 depth = active_count - rank - 1; Vector2 collapsed_position = { (float)p_camera->viewport_width - margin - width + (float)depth * 8.0f, (float)p_camera->viewport_height - margin - height - (float)depth * 8.0f, }; Vector2 expanded_position = { (float)p_camera->viewport_width - margin - width, (float)p_camera->viewport_height - margin - height - (float)depth * (height + gap), }; target_position = (Vector2){ collapsed_position.x + (expanded_position.x - collapsed_position.x) * eased, collapsed_position.y + (expanded_position.y - collapsed_position.y) * eased, }; target_size.x = width - (float)depth * 12.0f * (1.0f - eased); rank++; } else { target_position.x = (float)p_camera->viewport_width + 20.0f; } if (!p_entity->screen_space_initialized) { p_entity->pinned_screen_position = target_position; p_entity->pinned_screen_size = target_size; p_entity->screen_space_initialized = TRUE; } else { p_entity->pinned_screen_position.x += (target_position.x - p_entity->pinned_screen_position.x) * follow; p_entity->pinned_screen_position.y += (target_position.y - p_entity->pinned_screen_position.y) * follow; p_entity->pinned_screen_size.x += (target_size.x - p_entity->pinned_screen_size.x) * follow; p_entity->pinned_screen_size.y += (target_size.y - p_entity->pinned_screen_size.y) * follow; } p_entity->position = Canvas_Camera_Screen_To_World( p_camera, p_entity->pinned_screen_position); p_entity->size = (Vector2){ p_entity->pinned_screen_size.x / p_camera->zoom, p_entity->pinned_screen_size.y / p_camera->zoom, }; } } void Canvas_Scene_Sync_Pinned( Canvas_Scene *p_scene, const Canvas_Camera *p_camera) { for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { Canvas_Entity *p_entity = &p_scene->p_entities[index]; if (!p_entity->pinned) continue; Vector2 min_offset = {0.0f, 0.0f}; Vector2 max_offset = p_entity->pinned_screen_size; if (p_entity->type == CANVAS_ENTITY_CIRCLE) { min_offset = (Vector2){ -p_entity->pinned_screen_size.x, -p_entity->pinned_screen_size.x, }; max_offset = (Vector2){ p_entity->pinned_screen_size.x, p_entity->pinned_screen_size.x, }; } else if (p_entity->type == CANVAS_ENTITY_LINE) { min_offset = (Vector2){ fminf(0.0f, p_entity->pinned_screen_size.x), fminf(0.0f, p_entity->pinned_screen_size.y), }; max_offset = (Vector2){ fmaxf(0.0f, p_entity->pinned_screen_size.x), fmaxf(0.0f, p_entity->pinned_screen_size.y), }; } float min_x = -min_offset.x; float min_y = -min_offset.y; float max_x = fmaxf( min_x, (float)p_camera->viewport_width - max_offset.x); float max_y = fmaxf( min_y, (float)p_camera->viewport_height - max_offset.y); p_entity->pinned_screen_position.x = Canvas_Clamp( p_entity->pinned_screen_position.x, min_x, max_x); p_entity->pinned_screen_position.y = Canvas_Clamp( p_entity->pinned_screen_position.y, min_y, max_y); p_entity->position = Canvas_Camera_Screen_To_World( p_camera, p_entity->pinned_screen_position); p_entity->size = (Vector2){ p_entity->pinned_screen_size.x / p_camera->zoom, p_entity->pinned_screen_size.y / p_camera->zoom, }; } } void Canvas_Update_Camera_Input( Canvas_Camera *p_camera, boolean block_pointer_input, boolean block_keyboard_input) { boolean keyboard_pan = IsKeyDown(KEY_SPACE); boolean pointer_pan = IsMouseButtonDown(MOUSE_BUTTON_MIDDLE) || (keyboard_pan && IsMouseButtonDown(MOUSE_BUTTON_LEFT)); if (pointer_pan && !block_pointer_input) { Canvas_Camera_Pan(p_camera, GetMouseDelta()); } if (!block_pointer_input) { float wheel = GetMouseWheelMove(); if (wheel != 0.0f) { Canvas_Camera_Zoom_At( p_camera, GetMousePosition(), powf(1.15f, wheel)); } } if (!block_keyboard_input) { Vector2 keyboard_delta = {0.0f, 0.0f}; float speed = 600.0f * GetFrameTime(); if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT)) keyboard_delta.x += speed; if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT)) keyboard_delta.x -= speed; if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP)) keyboard_delta.y += speed; if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN)) keyboard_delta.y -= speed; Canvas_Camera_Pan(p_camera, keyboard_delta); } } static void Canvas_Draw_Grid( const Canvas_Camera *p_camera, const Canvas_Theme *p_theme) { Rectangle bounds = Canvas_Camera_Viewport_Bounds(p_camera); float spacing = Canvas_Grid_Spacing(p_camera->zoom); int32 first_x = (int32)floorf(bounds.x / spacing); int32 last_x = (int32)ceilf((bounds.x + bounds.width) / spacing); int32 first_y = (int32)floorf(bounds.y / spacing); int32 last_y = (int32)ceilf((bounds.y + bounds.height) / spacing); Color minor = Fade(p_theme->border, 0.36f); Color major = Fade(p_theme->border, 0.65f); Color axis = p_theme->text_muted; for (int32 index = first_x; index <= last_x; index++) { float x = (float)index * spacing; Color color = (index == 0) ? axis : ((index % 4 == 0) ? major : minor); DrawLineV( (Vector2){x, bounds.y}, (Vector2){x, bounds.y + bounds.height}, color); } for (int32 index = first_y; index <= last_y; index++) { float y = (float)index * spacing; Color color = (index == 0) ? axis : ((index % 4 == 0) ? major : minor); DrawLineV( (Vector2){bounds.x, y}, (Vector2){bounds.x + bounds.width, y}, color); } } static void Canvas_Draw_Selection( const Canvas_Entity *p_entity, float zoom) { float line = 2.0f / zoom; float padding = 7.0f / zoom; Color selection = {0, 122, 255, 230}; switch (p_entity->type) { case CANVAS_ENTITY_RECTANGLE: DrawRectangleRoundedLinesEx( (Rectangle){ p_entity->position.x - padding, p_entity->position.y - padding, p_entity->size.x + padding * 2.0f, p_entity->size.y + padding * 2.0f, }, 0.14f, 12, line, selection); break; case CANVAS_ENTITY_CIRCLE: DrawCircleLinesV( p_entity->position, p_entity->size.x + padding, selection); break; case CANVAS_ENTITY_LINE: DrawLineEx( p_entity->position, (Vector2){ p_entity->position.x + p_entity->size.x, p_entity->position.y + p_entity->size.y, }, 10.0f / zoom, Fade(selection, 0.35f)); break; case CANVAS_ENTITY_TEXT: case CANVAS_ENTITY_BUTTON: case CANVAS_ENTITY_TEXT_AREA: case CANVAS_ENTITY_DROPDOWN: case CANVAS_ENTITY_ACCORDION: case CANVAS_ENTITY_CARD: case CANVAS_ENTITY_CALENDAR: case CANVAS_ENTITY_SWITCH: case CANVAS_ENTITY_TABLE: case CANVAS_ENTITY_NOTIFICATION: case CANVAS_ENTITY_SCROLL_AREA: case CANVAS_ENTITY_IMAGE: { DrawRectangleRoundedLinesEx( (Rectangle){ p_entity->position.x - padding, p_entity->position.y - padding, p_entity->size.x + padding * 2.0f, p_entity->size.y + padding * 2.0f, }, 0.18f, 10, line, selection); break; } case CANVAS_ENTITY_WEB_CONTENT: DrawRectangleRoundedLinesEx( (Rectangle){ p_entity->position.x - padding, p_entity->position.y - padding, p_entity->size.x + padding * 2.0f, p_entity->size.y + padding * 2.0f, }, 0.04f, 10, line, selection); DrawRectangleRec( (Rectangle){ p_entity->position.x + p_entity->size.x - CANVAS_WEB_RESIZE_HANDLE / zoom, p_entity->position.y + p_entity->size.y - CANVAS_WEB_RESIZE_HANDLE / zoom, CANVAS_WEB_RESIZE_HANDLE / zoom, CANVAS_WEB_RESIZE_HANDLE / zoom, }, Fade(selection, 0.18f)); DrawLineEx( (Vector2){ p_entity->position.x + p_entity->size.x - 11.0f / zoom, p_entity->position.y + p_entity->size.y - 4.0f / zoom, }, (Vector2){ p_entity->position.x + p_entity->size.x - 4.0f / zoom, p_entity->position.y + p_entity->size.y - 11.0f / zoom, }, 1.5f / zoom, selection); break; default: break; } } static void Canvas_Draw_Entity( const Canvas_Entity *p_entity, Font font, float zoom, boolean selected, boolean pressed, const Canvas_Theme *p_theme) { float shadow_offset = 6.0f / zoom; Color shadow = p_theme->shadow; static const char *calendar_weekdays[] = { "S", "M", "T", "W", "T", "F", "S", }; switch (p_entity->type) { case CANVAS_ENTITY_RECTANGLE: Canvas_Theme_Draw_Shadow( p_theme, (Rectangle){ p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }, 0.14f, 12, zoom, 1.0f); DrawRectangleRounded( (Rectangle){ p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }, 0.14f, 12, Fade(p_entity->color, 0.92f)); DrawRectangleRoundedLinesEx( (Rectangle){ p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }, 0.14f, 12, 1.0f / zoom, Fade(BLACK, 0.08f)); break; case CANVAS_ENTITY_CIRCLE: DrawCircleV( (Vector2){ p_entity->position.x + shadow_offset, p_entity->position.y + shadow_offset, }, p_entity->size.x, shadow); DrawCircleV(p_entity->position, p_entity->size.x, p_entity->color); break; case CANVAS_ENTITY_LINE: DrawLineEx( p_entity->position, (Vector2){ p_entity->position.x + p_entity->size.x, p_entity->position.y + p_entity->size.y, }, 5.0f, p_entity->color); DrawCircleV(p_entity->position, 2.5f, p_entity->color); DrawCircleV( (Vector2){ p_entity->position.x + p_entity->size.x, p_entity->position.y + p_entity->size.y, }, 2.5f, p_entity->color); break; case CANVAS_ENTITY_TEXT: DrawTextEx( font, p_entity->text, p_entity->position, 28.0f, 0.5f, p_theme->text); break; case CANVAS_ENTITY_BUTTON: { float hover = p_entity->hover_amount; float grow = pressed ? -1.0f : hover * 2.0f; float lift = pressed ? 0.0f : hover * 2.0f; Rectangle bounds = { p_entity->position.x - grow, p_entity->position.y - grow - lift, p_entity->size.x + grow * 2.0f, p_entity->size.y + grow * 2.0f, }; Color base_fill = p_entity->active ? (Color){35, 99, 235, 255} : (Color){17, 24, 39, 255}; Color hover_fill = p_entity->active ? (Color){59, 130, 246, 255} : (Color){39, 48, 64, 255}; Color fill = Canvas_Color_Lerp(base_fill, hover_fill, hover); Canvas_Theme_Draw_Shadow( p_theme, bounds, 0.46f, 16, zoom, 1.0f); DrawRectangleRounded(bounds, 0.46f, 16, fill); Vector2 text_size = MeasureTextEx(font, p_entity->text, 16.0f, 0.1f); DrawTextEx( font, p_entity->text, (Vector2){ bounds.x + (bounds.width - text_size.x) * 0.5f, bounds.y + (bounds.height - text_size.y) * 0.5f, }, 16.0f, 0.1f, WHITE); break; } case CANVAS_ENTITY_TEXT_AREA: { Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.10f, 14, zoom, 1.0f); DrawRectangleRounded(bounds, 0.10f, 14, p_theme->surface); DrawRectangleRoundedLinesEx( bounds, 0.10f, 14, (p_entity->active ? 2.0f : 1.0f) / zoom, p_entity->active ? p_theme->accent : p_theme->border); DrawTextEx( font, p_entity->text[0] ? p_entity->text : "Write something...", (Vector2){bounds.x + 16.0f, bounds.y + 15.0f}, 16.0f, 0.1f, p_entity->text[0] ? p_theme->text : p_theme->text_muted); if (p_entity->active) { DrawCircleV( (Vector2){bounds.x + bounds.width - 16.0f, bounds.y + 16.0f}, 3.0f, p_theme->accent); } break; } case CANVAS_ENTITY_DROPDOWN: { Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.30f, 14, zoom, 1.0f); DrawRectangleRounded(bounds, 0.30f, 14, p_theme->surface); DrawRectangleRoundedLinesEx( bounds, 0.30f, 14, 1.0f / zoom, p_theme->border); DrawTextEx( font, CANVAS_DROPDOWN_OPTIONS[p_entity->value], (Vector2){bounds.x + 16.0f, bounds.y + 14.0f}, 16.0f, 0.1f, p_theme->text); Vector2 chevron = {bounds.x + bounds.width - 22.0f, bounds.y + bounds.height * 0.5f}; DrawTriangle( (Vector2){chevron.x - 5.0f, chevron.y - 2.0f}, (Vector2){chevron.x, chevron.y + 4.0f}, (Vector2){chevron.x + 5.0f, chevron.y - 2.0f}, p_theme->text_muted); break; } case CANVAS_ENTITY_ACCORDION: { Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.10f, 14, zoom, 1.0f); DrawRectangleRounded(bounds, 0.10f, 14, p_theme->surface); DrawRectangleRoundedLinesEx( bounds, 0.10f, 14, 1.0f / zoom, p_theme->border); DrawTextEx( font, p_entity->text, (Vector2){bounds.x + 16.0f, bounds.y + 18.0f}, 16.0f, 0.1f, p_theme->text); float amount = p_entity->animation_amount; float angle = amount * PI * 0.5f; Vector2 chevron = {bounds.x + bounds.width - 22.0f, bounds.y + 28.0f}; Vector2 direction = {cosf(angle), sinf(angle)}; Vector2 normal = {-direction.y, direction.x}; Vector2 tip = { chevron.x + direction.x * 3.0f, chevron.y + direction.y * 3.0f, }; DrawLineEx( tip, (Vector2){ chevron.x - direction.x * 3.0f + normal.x * 4.0f, chevron.y - direction.y * 3.0f + normal.y * 4.0f, }, 1.5f / zoom, p_theme->text_muted); DrawLineEx( tip, (Vector2){ chevron.x - direction.x * 3.0f - normal.x * 4.0f, chevron.y - direction.y * 3.0f - normal.y * 4.0f, }, 1.5f / zoom, p_theme->text_muted); if (amount > 0.0f) { DrawLineEx( (Vector2){bounds.x, bounds.y + 56.0f}, (Vector2){bounds.x + bounds.width, bounds.y + 56.0f}, 1.0f / zoom, Fade(p_theme->border, amount)); if (amount > 0.55f) { float body_alpha = Canvas_Clamp( (amount - 0.55f) / 0.45f, 0.0f, 1.0f); DrawTextEx( font, "Drag, zoom, pin, and combine primitives\nacross one continuous workspace.", (Vector2){bounds.x + 16.0f, bounds.y + 76.0f}, 14.0f, 0.1f, Fade(p_theme->text_muted, body_alpha)); } } break; } case CANVAS_ENTITY_CARD: { Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.08f, 14, zoom, 1.0f); DrawRectangleRounded(bounds, 0.08f, 14, p_theme->surface); DrawRectangleRounded( (Rectangle){bounds.x + 12.0f, bounds.y + 12.0f, bounds.width - 24.0f, 92.0f}, 0.08f, 12, p_entity->active ? p_theme->accent_soft : Fade(p_entity->color, 0.22f)); DrawCircleV( (Vector2){bounds.x + 44.0f, bounds.y + 58.0f}, 18.0f, p_entity->active ? (Color){37, 99, 235, 255} : p_entity->color); DrawTextEx( font, p_entity->text, (Vector2){bounds.x + 16.0f, bounds.y + 122.0f}, 20.0f, 0.1f, p_theme->text); DrawTextEx( font, "A flexible surface for tools,\ncontent, and live browser views.", (Vector2){bounds.x + 16.0f, bounds.y + 154.0f}, 14.0f, 0.1f, p_theme->text_muted); DrawRectangleRoundedLinesEx( bounds, 0.08f, 14, (p_entity->active ? 2.0f : 1.0f) / zoom, p_entity->active ? p_theme->accent : p_theme->border); break; } case CANVAS_ENTITY_CALENDAR: { Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.08f, 14, zoom, 1.0f); DrawRectangleRounded(bounds, 0.08f, 14, p_theme->surface); DrawRectangleRoundedLinesEx( bounds, 0.08f, 14, 1.0f / zoom, p_theme->border); DrawTextEx( font, "August 2026", (Vector2){bounds.x + 16.0f, bounds.y + 18.0f}, 20.0f, 0.1f, p_theme->text); for (int32 column = 0; column < 7; column++) { float cell_x = bounds.x + 14.0f + (float)column * 36.0f; DrawTextEx( font, calendar_weekdays[column], (Vector2){cell_x + 12.0f, bounds.y + 56.0f}, 12.0f, 0.0f, p_theme->text_muted); } for (int32 day = 1; day <= 31; day++) { int32 cell = day + 6 - 1; int32 row = cell / 7; int32 column = cell % 7; Vector2 center = { bounds.x + 14.0f + (float)column * 36.0f + 18.0f, bounds.y + 82.0f + (float)row * 32.0f + 16.0f, }; if (day == p_entity->value) { DrawCircleV(center, 14.0f, p_theme->accent); } const char *label = TextFormat("%d", day); Vector2 label_size = MeasureTextEx(font, label, 13.0f, 0.0f); DrawTextEx( font, label, (Vector2){center.x - label_size.x * 0.5f, center.y - label_size.y * 0.5f}, 13.0f, 0.0f, day == p_entity->value ? WHITE : p_theme->text); } break; } case CANVAS_ENTITY_SWITCH: { float amount = p_entity->animation_amount; float eased = amount * amount * (3.0f - 2.0f * amount); Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Color track = Canvas_Color_Lerp( p_theme->border, p_theme->accent, eased); if (!p_entity->active) { track = Canvas_Color_Lerp( track, p_theme->text_muted, p_entity->hover_amount); } Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.50f, 18, zoom, 1.0f); DrawRectangleRounded(bounds, 0.50f, 18, track); float knob_x = bounds.x + 18.0f + eased * (bounds.width - 36.0f); float knob_radius = pressed ? 12.5f : 14.0f; DrawCircleV( (Vector2){knob_x, bounds.y + bounds.height * 0.5f}, knob_radius, WHITE); break; } case CANVAS_ENTITY_TABLE: { Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.06f, 12, zoom, 1.0f); DrawRectangleRounded(bounds, 0.06f, 12, p_theme->surface); DrawRectangleRoundedLinesEx( bounds, 0.06f, 12, 1.0f / zoom, p_theme->border); DrawTextEx( font, "Name", (Vector2){bounds.x + 16.0f, bounds.y + 22.0f}, 13.0f, 0.0f, p_theme->text_muted); DrawTextEx( font, "Status", (Vector2){bounds.x + 292.0f, bounds.y + 22.0f}, 13.0f, 0.0f, p_theme->text_muted); DrawLineEx( (Vector2){bounds.x, bounds.y + 52.0f}, (Vector2){bounds.x + bounds.width, bounds.y + 52.0f}, 1.0f / zoom, p_theme->border); for (int32 row = 0; row < 4; row++) { float row_y = bounds.y + 62.0f + (float)row * 38.0f; if (row == p_entity->value) { DrawRectangle( (int32)(bounds.x + 8.0f), (int32)(row_y - 4.0f), (int32)(bounds.width - 16.0f), 34, p_theme->accent_soft); } DrawTextEx( font, CANVAS_TABLE_NAMES[row], (Vector2){bounds.x + 16.0f, row_y + 5.0f}, 14.0f, 0.0f, p_theme->text); DrawTextEx( font, CANVAS_TABLE_STATUSES[row], (Vector2){bounds.x + 292.0f, row_y + 5.0f}, 14.0f, 0.0f, row == p_entity->value ? p_theme->accent : p_theme->text_muted); } break; } case CANVAS_ENTITY_NOTIFICATION: { if (p_entity->animation_amount <= 0.01f) break; float unit = 1.0f / zoom; Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; float opacity = p_entity->animation_amount; Canvas_Theme_Draw_Shadow( p_theme, bounds, 0.20f, 16, zoom, opacity); DrawRectangleRounded( bounds, 0.20f, 16, Fade(p_theme->toast_surface, opacity)); DrawRectangleRoundedLinesEx( bounds, 0.20f, 16, 1.0f / zoom, Fade(p_theme->toast_border, opacity)); DrawTextEx( font, p_entity->text, (Vector2){bounds.x + 16.0f * unit, bounds.y + 18.0f * unit}, 15.0f * unit, 0.0f, Fade(p_theme->text, opacity)); DrawTextEx( font, "Sunday, December 3 at 9:00 AM", (Vector2){bounds.x + 16.0f * unit, bounds.y + 45.0f * unit}, 14.0f * unit, 0.0f, Fade(p_theme->text_muted, opacity)); Rectangle undo = { bounds.x + bounds.width - 108.0f * unit, bounds.y + 24.0f * unit, 54.0f * unit, 34.0f * unit, }; DrawRectangleRounded( undo, 0.26f, 10, Fade(p_theme->surface_muted, opacity)); DrawRectangleRoundedLinesEx( undo, 0.26f, 10, 1.0f / zoom, Fade(p_theme->border, opacity)); DrawTextEx( font, "Undo", (Vector2){undo.x + 12.0f * unit, undo.y + 10.0f * unit}, 13.0f * unit, 0.0f, Fade(p_theme->text, opacity)); Vector2 close = { bounds.x + bounds.width - 24.0f * unit, bounds.y + bounds.height * 0.5f, }; DrawLineEx( (Vector2){close.x - 4.0f * unit, close.y - 4.0f * unit}, (Vector2){close.x + 4.0f * unit, close.y + 4.0f * unit}, 1.5f / zoom, Fade(p_theme->text_muted, opacity)); DrawLineEx( (Vector2){close.x + 4.0f * unit, close.y - 4.0f * unit}, (Vector2){close.x - 4.0f * unit, close.y + 4.0f * unit}, 1.5f / zoom, Fade(p_theme->text_muted, opacity)); break; } case CANVAS_ENTITY_SCROLL_AREA: { Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.08f, 12, zoom, 1.0f); DrawRectangleRounded(bounds, 0.08f, 12, p_theme->surface); DrawRectangleRoundedLinesEx( bounds, 0.08f, 12, 1.0f / zoom, p_theme->border); DrawTextEx( font, "Scrollable area", (Vector2){bounds.x + 16.0f, bounds.y + 16.0f}, 18.0f, 0.0f, p_theme->text); for (int32 item = 0; item < 10; item++) { float item_y = bounds.y + 52.0f + (float)item * 42.0f - (float)p_entity->value; if (item_y < bounds.y + 46.0f || item_y + 34.0f > bounds.y + bounds.height - 12.0f) { continue; } DrawRectangleRounded( (Rectangle){bounds.x + 14.0f, item_y, bounds.width - 38.0f, 34.0f}, 0.16f, 8, item % 2 == 0 ? p_theme->surface_muted : Canvas_Color_Lerp(p_theme->surface, p_theme->surface_muted, 0.5f)); DrawTextEx( font, TextFormat("Item %d", item + 1), (Vector2){bounds.x + 26.0f, item_y + 9.0f}, 13.0f, 0.0f, p_theme->text); } float thumb_y = bounds.y + 52.0f + ((float)p_entity->value / 240.0f) * (bounds.height - 106.0f); DrawRectangleRounded( (Rectangle){bounds.x + bounds.width - 14.0f, thumb_y, 5.0f, 42.0f}, 0.50f, 6, Fade(p_theme->text_muted, 0.82f)); break; } case CANVAS_ENTITY_IMAGE: { Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.04f, 12, zoom, 1.0f); DrawRectangleRounded(bounds, 0.04f, 12, p_theme->surface_muted); DrawRectangleRoundedLinesEx( bounds, 0.04f, 12, 1.0f / zoom, p_theme->border); DrawTextEx( font, "Loading image...", (Vector2){bounds.x + 16.0f, bounds.y + 16.0f}, 14.0f, 0.0f, p_theme->text_muted); break; } case CANVAS_ENTITY_WEB_CONTENT: { Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.04f, 14, zoom, 1.0f); DrawRectangleRounded(bounds, 0.04f, 14, p_theme->surface_muted); DrawRectangleRoundedLinesEx( bounds, 0.04f, 14, 1.0f / zoom, p_theme->border); #if !defined(PLATFORM_WEB) && !defined(__EMSCRIPTEN__) Vector2 label_size = MeasureTextEx( font, "Loading browser surface...", 18.0f, 0.1f); DrawTextEx( font, "Loading browser surface...", (Vector2){ bounds.x + (bounds.width - label_size.x) * 0.5f, bounds.y + (bounds.height - label_size.y) * 0.5f, }, 18.0f, 0.1f, p_theme->text_muted); #endif break; } default: break; } if (p_entity->pinned) { float radius = 7.0f / zoom; Vector2 center = { p_entity->position.x + p_entity->size.x - 10.0f / zoom, p_entity->position.y + 10.0f / zoom, }; DrawCircleV(center, radius, p_theme->accent); DrawLineEx( (Vector2){center.x, center.y - 3.5f / zoom}, (Vector2){center.x, center.y + 4.0f / zoom}, 1.5f / zoom, WHITE); } if (selected && p_entity->type != CANVAS_ENTITY_NOTIFICATION) { Canvas_Draw_Selection(p_entity, zoom); } } static void Canvas_Draw_Dropdown_Menu( const Canvas_Entity *p_entity, Font font, Vector2 world_pointer, float zoom, const Canvas_Theme *p_theme) { static const char *options[] = { "Design", "Engineering", "Research", }; Rectangle menu = { p_entity->position.x, p_entity->position.y + p_entity->size.y + 8.0f, p_entity->size.x, 120.0f, }; Canvas_Theme_Draw_Shadow(p_theme, menu, 0.12f, 14, zoom, 1.0f); DrawRectangleRounded(menu, 0.12f, 14, p_theme->surface); DrawRectangleRoundedLinesEx( menu, 0.12f, 14, 1.0f / zoom, p_theme->border); for (int32 option = 0; option < 3; option++) { Rectangle row = { menu.x + 5.0f, menu.y + 4.0f + (float)option * 38.0f, menu.width - 10.0f, 36.0f, }; boolean hovered = CheckCollisionPointRec(world_pointer, row) ? TRUE : FALSE; boolean selected = p_entity->value == option; if (hovered || selected) { DrawRectangleRounded( row, 0.20f, 10, hovered ? p_theme->accent_soft : p_theme->surface_muted); } DrawTextEx( font, options[option], (Vector2){row.x + 12.0f, row.y + 9.0f}, 15.0f, 0.1f, selected ? p_theme->accent : p_theme->text); if (selected) { DrawCircleV( (Vector2){row.x + row.width - 14.0f, row.y + row.height * 0.5f}, 3.0f, p_theme->accent); } } } void Canvas_Draw_World( const Canvas_Camera *p_camera, const Canvas_Scene *p_scene, Font font, const Canvas_Theme *p_theme) { Vector2 world_pointer = Canvas_Camera_Screen_To_World(p_camera, GetMousePosition()); Camera2D camera = { .offset = { (float)p_camera->viewport_width * 0.5f, (float)p_camera->viewport_height * 0.5f, }, .target = p_camera->target, .rotation = 0.0f, .zoom = p_camera->zoom, }; BeginMode2D(camera); if (p_scene->show_grid) Canvas_Draw_Grid(p_camera, p_theme); for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { Canvas_Draw_Entity( &p_scene->p_entities[index], font, p_camera->zoom, (int32)index == p_scene->selected_index, (int32)index == p_scene->pressed_index && IsMouseButtonDown(MOUSE_BUTTON_LEFT), p_theme); } for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { const Canvas_Entity *p_entity = &p_scene->p_entities[index]; if (p_entity->type == CANVAS_ENTITY_DROPDOWN && p_entity->active) { Canvas_Draw_Dropdown_Menu( p_entity, font, world_pointer, p_camera->zoom, p_theme); } } EndMode2D(); } const char *Canvas_Entity_Type_Name(Canvas_Entity_Type type) { switch (type) { case CANVAS_ENTITY_RECTANGLE: return "Rectangle"; case CANVAS_ENTITY_CIRCLE: return "Circle"; case CANVAS_ENTITY_LINE: return "Line"; case CANVAS_ENTITY_TEXT: return "Text"; case CANVAS_ENTITY_BUTTON: return "Button"; case CANVAS_ENTITY_TEXT_AREA: return "Text area"; case CANVAS_ENTITY_DROPDOWN: return "Dropdown"; case CANVAS_ENTITY_ACCORDION: return "Accordion"; case CANVAS_ENTITY_CARD: return "Card"; case CANVAS_ENTITY_CALENDAR: return "Calendar"; case CANVAS_ENTITY_SWITCH: return "Switch"; case CANVAS_ENTITY_TABLE: return "Table"; case CANVAS_ENTITY_NOTIFICATION: return "Notification"; case CANVAS_ENTITY_SCROLL_AREA: return "Scrollable area"; case CANVAS_ENTITY_IMAGE: return "Image"; case CANVAS_ENTITY_WEB_CONTENT: return "Web content"; default: return "Unknown"; } }