Mercurial
diff 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 diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/infinite_canvas/canvas.c Mon Aug 17 16:57:56 2026 -0700 @@ -0,0 +1,2104 @@ +#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"; + } +}