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";
+    }
+}