view infinite_canvas/canvas.c @ 276:b55c22cff335

Add interactive infinite canvas prototype
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 16:57:56 -0700
parents
children 8d560f50ed4c
line wrap: on
line source

#include "infinite_canvas/canvas.h"

#include <math.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>

static const char *CANVAS_DROPDOWN_OPTIONS[] = {
    "Design",
    "Engineering",
    "Research",
};

static const char *CANVAS_TABLE_NAMES[] = {
    "Canvas",
    "Browser",
    "Notes",
    "Assets",
};

static const char *CANVAS_TABLE_STATUSES[] = {
    "Ready",
    "Live",
    "Draft",
    "Synced",
};

static float Canvas_Clamp(float value, float min_value, float max_value)
{
    if (value < min_value) return min_value;
    if (value > max_value) return max_value;
    return value;
}

static Color Canvas_Entity_Color(uint32 index)
{
    static const Color colors[] = {
        {96, 165, 250, 255},
        {251, 191, 36, 255},
        {167, 139, 250, 255},
        {45, 212, 191, 255},
        {251, 113, 133, 255},
    };
    return colors[index % (sizeof(colors) / sizeof(colors[0]))];
}

static Color Canvas_Color_Lerp(Color from, Color to, float amount)
{
    amount = Canvas_Clamp(amount, 0.0f, 1.0f);
    return (Color){
        (uint8)((float)from.r + ((float)to.r - (float)from.r) * amount),
        (uint8)((float)from.g + ((float)to.g - (float)from.g) * amount),
        (uint8)((float)from.b + ((float)to.b - (float)from.b) * amount),
        (uint8)((float)from.a + ((float)to.a - (float)from.a) * amount),
    };
}

void Canvas_Camera_Init(Canvas_Camera *p_camera, int32 width, int32 height)
{
    *p_camera = (Canvas_Camera){
        .target = {0.0f, 0.0f},
        .zoom = 1.0f,
        .viewport_width = width,
        .viewport_height = height,
        .min_zoom = 0.01f,
        .max_zoom = 64.0f,
    };
}

void Canvas_Camera_Set_Viewport(Canvas_Camera *p_camera, int32 width, int32 height)
{
    p_camera->viewport_width = width;
    p_camera->viewport_height = height;
}

Vector2 Canvas_Camera_Screen_To_World(const Canvas_Camera *p_camera, Vector2 screen)
{
    Vector2 center = {
        (float)p_camera->viewport_width * 0.5f,
        (float)p_camera->viewport_height * 0.5f,
    };
    return (Vector2){
        p_camera->target.x + (screen.x - center.x) / p_camera->zoom,
        p_camera->target.y + (screen.y - center.y) / p_camera->zoom,
    };
}

Vector2 Canvas_Camera_World_To_Screen(const Canvas_Camera *p_camera, Vector2 world)
{
    Vector2 center = {
        (float)p_camera->viewport_width * 0.5f,
        (float)p_camera->viewport_height * 0.5f,
    };
    return (Vector2){
        center.x + (world.x - p_camera->target.x) * p_camera->zoom,
        center.y + (world.y - p_camera->target.y) * p_camera->zoom,
    };
}

void Canvas_Camera_Pan(Canvas_Camera *p_camera, Vector2 screen_delta)
{
    p_camera->target.x -= screen_delta.x / p_camera->zoom;
    p_camera->target.y -= screen_delta.y / p_camera->zoom;
}

void Canvas_Camera_Zoom_At(
    Canvas_Camera *p_camera,
    Vector2 screen_anchor,
    float zoom_multiplier)
{
    Vector2 world_anchor = Canvas_Camera_Screen_To_World(p_camera, screen_anchor);
    p_camera->zoom = Canvas_Clamp(
        p_camera->zoom * zoom_multiplier,
        p_camera->min_zoom,
        p_camera->max_zoom);
    Vector2 moved_anchor = Canvas_Camera_Screen_To_World(p_camera, screen_anchor);
    p_camera->target.x += world_anchor.x - moved_anchor.x;
    p_camera->target.y += world_anchor.y - moved_anchor.y;
}

Rectangle Canvas_Camera_Viewport_Bounds(const Canvas_Camera *p_camera)
{
    Vector2 min = Canvas_Camera_Screen_To_World(p_camera, (Vector2){0.0f, 0.0f});
    Vector2 max = Canvas_Camera_Screen_To_World(
        p_camera,
        (Vector2){(float)p_camera->viewport_width, (float)p_camera->viewport_height});
    return (Rectangle){
        .x = min.x,
        .y = min.y,
        .width = max.x - min.x,
        .height = max.y - min.y,
    };
}

static Rectangle Canvas_Entity_Bounds(const Canvas_Entity *p_entity)
{
    if (p_entity->type == CANVAS_ENTITY_CIRCLE) {
        return (Rectangle){
            p_entity->position.x - p_entity->size.x,
            p_entity->position.y - p_entity->size.x,
            p_entity->size.x * 2.0f,
            p_entity->size.x * 2.0f,
        };
    }
    if (p_entity->type == CANVAS_ENTITY_LINE) {
        float end_x = p_entity->position.x + p_entity->size.x;
        float end_y = p_entity->position.y + p_entity->size.y;
        return (Rectangle){
            fminf(p_entity->position.x, end_x),
            fminf(p_entity->position.y, end_y),
            fmaxf(fabsf(p_entity->size.x), 0.001f),
            fmaxf(fabsf(p_entity->size.y), 0.001f),
        };
    }

    Rectangle bounds = {
        p_entity->position.x,
        p_entity->position.y,
        p_entity->size.x,
        p_entity->size.y,
    };
    if (p_entity->type == CANVAS_ENTITY_DROPDOWN && p_entity->active) {
        bounds.height += 128.0f;
    }
    return bounds;
}

typedef struct {
    char *p_buffer;
    size_t capacity;
    size_t length;
    boolean truncated;
} Canvas_Context_Writer;

static void Canvas_Context_Append(
    Canvas_Context_Writer *p_writer,
    const char *p_format,
    ...)
{
    if (p_writer->truncated || p_writer->capacity == 0) {
        p_writer->truncated = TRUE;
        return;
    }

    va_list arguments;
    va_start(arguments, p_format);
    int32 written = vsnprintf(
        p_writer->p_buffer + p_writer->length,
        p_writer->capacity - p_writer->length,
        p_format,
        arguments);
    va_end(arguments);
    if (written < 0) {
        p_writer->truncated = TRUE;
        return;
    }

    size_t available = p_writer->capacity - p_writer->length;
    if ((size_t)written >= available) {
        p_writer->length = p_writer->capacity - 1;
        p_writer->truncated = TRUE;
        return;
    }
    p_writer->length += (size_t)written;
}

static void Canvas_Context_Append_Quoted(
    Canvas_Context_Writer *p_writer,
    const char *p_text)
{
    Canvas_Context_Append(p_writer, "\"");
    for (const char *p_cursor = p_text; *p_cursor && !p_writer->truncated; p_cursor++) {
        if (*p_cursor == '"' || *p_cursor == '\\') {
            Canvas_Context_Append(p_writer, "\\%c", *p_cursor);
        } else if (*p_cursor == '\n') {
            Canvas_Context_Append(p_writer, "\\n");
        } else if (*p_cursor == '\r') {
            continue;
        } else {
            Canvas_Context_Append(p_writer, "%c", *p_cursor);
        }
    }
    Canvas_Context_Append(p_writer, "\"");
}

static void Canvas_Context_Append_Entity_State(
    Canvas_Context_Writer *p_writer,
    const Canvas_Entity *p_entity)
{
    switch (p_entity->type) {
        case CANVAS_ENTITY_RECTANGLE:
        case CANVAS_ENTITY_CIRCLE:
        case CANVAS_ENTITY_LINE:
            Canvas_Context_Append(
                p_writer,
                "color=rgba(%u,%u,%u,%u)",
                p_entity->color.r,
                p_entity->color.g,
                p_entity->color.b,
                p_entity->color.a);
            break;
        case CANVAS_ENTITY_TEXT:
            Canvas_Context_Append(p_writer, "content=");
            Canvas_Context_Append_Quoted(p_writer, p_entity->text);
            break;
        case CANVAS_ENTITY_BUTTON:
            Canvas_Context_Append(p_writer, "label=");
            Canvas_Context_Append_Quoted(p_writer, p_entity->text);
            Canvas_Context_Append(
                p_writer,
                " activated=%s",
                p_entity->active ? "true" : "false");
            break;
        case CANVAS_ENTITY_TEXT_AREA:
            Canvas_Context_Append(p_writer, "content=");
            Canvas_Context_Append_Quoted(p_writer, p_entity->text);
            Canvas_Context_Append(
                p_writer,
                " editing=%s",
                p_entity->active ? "true" : "false");
            break;
        case CANVAS_ENTITY_DROPDOWN:
            Canvas_Context_Append(p_writer, "selected=");
            Canvas_Context_Append_Quoted(
                p_writer,
                CANVAS_DROPDOWN_OPTIONS[p_entity->value]);
            Canvas_Context_Append(
                p_writer,
                " open=%s",
                p_entity->active ? "true" : "false");
            break;
        case CANVAS_ENTITY_ACCORDION:
            Canvas_Context_Append(p_writer, "title=");
            Canvas_Context_Append_Quoted(p_writer, p_entity->text);
            Canvas_Context_Append(
                p_writer,
                " expanded=%s",
                p_entity->active ? "true" : "false");
            break;
        case CANVAS_ENTITY_CARD:
            Canvas_Context_Append(p_writer, "title=");
            Canvas_Context_Append_Quoted(p_writer, p_entity->text);
            Canvas_Context_Append(
                p_writer,
                " emphasized=%s",
                p_entity->active ? "true" : "false");
            break;
        case CANVAS_ENTITY_CALENDAR:
            Canvas_Context_Append(
                p_writer,
                "selected_date=\"2026-08-%02d\"",
                p_entity->value);
            break;
        case CANVAS_ENTITY_SWITCH:
            Canvas_Context_Append(
                p_writer,
                "checked=%s",
                p_entity->active ? "true" : "false");
            break;
        case CANVAS_ENTITY_TABLE:
            Canvas_Context_Append(p_writer, "selected_row={name=");
            Canvas_Context_Append_Quoted(
                p_writer,
                CANVAS_TABLE_NAMES[p_entity->value]);
            Canvas_Context_Append(p_writer, ", status=");
            Canvas_Context_Append_Quoted(
                p_writer,
                CANVAS_TABLE_STATUSES[p_entity->value]);
            Canvas_Context_Append(p_writer, "}");
            break;
        case CANVAS_ENTITY_NOTIFICATION:
            Canvas_Context_Append(p_writer, "title=");
            Canvas_Context_Append_Quoted(p_writer, p_entity->text);
            Canvas_Context_Append(
                p_writer,
                " active=%s",
                p_entity->active ? "true" : "false");
            break;
        case CANVAS_ENTITY_SCROLL_AREA: {
            int32 first_item = p_entity->value / 42 + 1;
            int32 last_item = first_item + 3;
            if (last_item > 10) last_item = 10;
            Canvas_Context_Append(
                p_writer,
                "scroll_offset=%d visible_items=%d-%d",
                p_entity->value,
                first_item,
                last_item);
            break;
        }
        case CANVAS_ENTITY_IMAGE:
            Canvas_Context_Append(p_writer, "source=");
            Canvas_Context_Append_Quoted(p_writer, p_entity->text);
            break;
        case CANVAS_ENTITY_WEB_CONTENT:
            Canvas_Context_Append(p_writer, "url=");
            Canvas_Context_Append_Quoted(p_writer, p_entity->text);
            break;
        default:
            Canvas_Context_Append(p_writer, "value=%d", p_entity->value);
            break;
    }
}

Canvas_Context_Snapshot Canvas_Scene_Build_Visible_Context(
    const Canvas_Scene *p_scene,
    const Canvas_Camera *p_camera,
    char *p_buffer,
    size_t buffer_size)
{
    Canvas_Context_Writer writer = {
        .p_buffer = p_buffer,
        .capacity = buffer_size,
    };
    if (buffer_size > 0) p_buffer[0] = '\0';

    Rectangle viewport = Canvas_Camera_Viewport_Bounds(p_camera);
    Canvas_Context_Append(
        &writer,
        "camera_context zoom=%.2f viewport=[%.1f, %.1f, %.1f, %.1f]\n"
        "entities:\n",
        p_camera->zoom,
        viewport.x,
        viewport.y,
        viewport.width,
        viewport.height);

    Canvas_Context_Snapshot snapshot = {0};
    for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
        const Canvas_Entity *p_entity = &p_scene->p_entities[index];
        if (p_entity->type == CANVAS_ENTITY_NOTIFICATION &&
            p_entity->animation_amount <= 0.01f) {
            continue;
        }
        if (!CheckCollisionRecs(viewport, Canvas_Entity_Bounds(p_entity))) continue;

        snapshot.visible_count++;
        Canvas_Context_Append(
            &writer,
            "- id=%u type=\"%s\" position=[%.1f, %.1f] size=[%.1f, %.1f] pinned=%s\n"
            "  state: ",
            p_entity->id,
            Canvas_Entity_Type_Name(p_entity->type),
            p_entity->position.x,
            p_entity->position.y,
            p_entity->size.x,
            p_entity->size.y,
            p_entity->pinned ? "true" : "false");
        Canvas_Context_Append_Entity_State(&writer, p_entity);
        Canvas_Context_Append(&writer, "\n");
    }
    if (snapshot.visible_count == 0) {
        Canvas_Context_Append(&writer, "  (none)\n");
    }

    snapshot.bytes_written = writer.length;
    snapshot.truncated = writer.truncated;
    return snapshot;
}

float Canvas_Grid_Spacing(float zoom)
{
    float spacing = 64.0f;
    while ((spacing * zoom < 48.0f) && (spacing < 1048576.0f)) spacing *= 2.0f;
    while ((spacing * zoom > 96.0f) && (spacing > 0.0009765625f)) spacing *= 0.5f;
    return spacing;
}

void Canvas_Scene_Init(Canvas_Scene *p_scene, Dowa_Arena *p_arena)
{
    memset(p_scene, 0, sizeof(*p_scene));
    p_scene->p_arena = p_arena;
    p_scene->selected_index = -1;
    p_scene->pressed_index = -1;
    p_scene->hover_index = -1;
    p_scene->show_grid = FALSE;
    Dowa_Array_Reserve_Arena(p_scene->p_entities, CANVAS_MAX_ENTITIES, p_arena);
}

boolean Canvas_Scene_Add(Canvas_Scene *p_scene, Canvas_Entity_Type type, Vector2 position)
{
    if (Dowa_Array_Length(p_scene->p_entities) >= CANVAS_MAX_ENTITIES) return FALSE;

    uint32 index = p_scene->next_entity++;
    Canvas_Entity entity = {
        .id = ++p_scene->next_entity_id,
        .type = type,
        .position = position,
        .size = {160.0f, 100.0f},
        .color = Canvas_Entity_Color(index),
        .text = {0},
        .value = 0,
        .active = FALSE,
        .pinned = FALSE,
        .hover_amount = 0.0f,
        .animation_amount = 0.0f,
    };

    if (type == CANVAS_ENTITY_CIRCLE) entity.size = (Vector2){56.0f, 56.0f};
    if (type == CANVAS_ENTITY_LINE) entity.size = (Vector2){180.0f, 80.0f};
    if (type == CANVAS_ENTITY_TEXT) {
        entity.size = (Vector2){180.0f, 32.0f};
        snprintf(entity.text, sizeof(entity.text), "Entity %u", index + 1);
    }
    if (type == CANVAS_ENTITY_BUTTON) {
        entity.size = (Vector2){156.0f, 48.0f};
        snprintf(entity.text, sizeof(entity.text), "Continue");
    }
    if (type == CANVAS_ENTITY_TEXT_AREA) {
        entity.size = (Vector2){280.0f, 148.0f};
    }
    if (type == CANVAS_ENTITY_DROPDOWN) {
        entity.size = (Vector2){224.0f, 48.0f};
    }
    if (type == CANVAS_ENTITY_ACCORDION) {
        entity.size = (Vector2){300.0f, 56.0f};
        snprintf(entity.text, sizeof(entity.text), "How does the canvas work?");
    }
    if (type == CANVAS_ENTITY_CARD) {
        entity.size = (Vector2){280.0f, 220.0f};
        snprintf(entity.text, sizeof(entity.text), "Infinite workspace");
    }
    if (type == CANVAS_ENTITY_CALENDAR) {
        entity.size = (Vector2){280.0f, 300.0f};
        entity.value = 17;
    }
    if (type == CANVAS_ENTITY_SWITCH) {
        entity.size = (Vector2){64.0f, 36.0f};
    }
    if (type == CANVAS_ENTITY_TABLE) {
        entity.size = (Vector2){420.0f, 230.0f};
        entity.value = 1;
    }
    if (type == CANVAS_ENTITY_NOTIFICATION) {
        entity.size = (Vector2){384.0f, 82.0f};
        entity.active = TRUE;
        entity.animation_amount = 1.0f;
        snprintf(entity.text, sizeof(entity.text), "Event created");
    }
    if (type == CANVAS_ENTITY_SCROLL_AREA) {
        entity.size = (Vector2){320.0f, 220.0f};
    }
    if (type == CANVAS_ENTITY_IMAGE) {
        entity.size = (Vector2){360.0f, 240.0f};
        snprintf(
            entity.text,
            sizeof(entity.text),
            "infinite_canvas/assets/image-placeholder.svg");
    }
    if (type == CANVAS_ENTITY_WEB_CONTENT) {
        entity.size = (Vector2){720.0f, 460.0f};
        entity.active = TRUE;
        snprintf(entity.text, sizeof(entity.text), "https://mrjunejune.com");
    }

    Dowa_Array_Push_Arena(p_scene->p_entities, entity, p_scene->p_arena);
    return TRUE;
}

void Canvas_Scene_Clear(Canvas_Scene *p_scene)
{
    Dowa_Array_Clear(p_scene->p_entities);
    p_scene->next_entity = 0;
    p_scene->selected_index = -1;
    p_scene->pressed_index = -1;
    p_scene->hover_index = -1;
    p_scene->dragging = FALSE;
    p_scene->resizing = FALSE;
    p_scene->notification_stack_amount = 0.0f;
}

void Canvas_Scene_Add_Demo(Canvas_Scene *p_scene)
{
    Canvas_Scene_Clear(p_scene);
    Canvas_Scene_Add(p_scene, CANVAS_ENTITY_RECTANGLE, (Vector2){-550.0f, -520.0f});
    Canvas_Scene_Add(p_scene, CANVAS_ENTITY_CIRCLE, (Vector2){-300.0f, -470.0f});
    Canvas_Scene_Add(p_scene, CANVAS_ENTITY_LINE, (Vector2){-180.0f, -500.0f});
    Canvas_Scene_Add(p_scene, CANVAS_ENTITY_TEXT, (Vector2){50.0f, -500.0f});
    Canvas_Scene_Add(p_scene, CANVAS_ENTITY_BUTTON, (Vector2){280.0f, -510.0f});
    Canvas_Scene_Add(p_scene, CANVAS_ENTITY_SWITCH, (Vector2){500.0f, -504.0f});

    Canvas_Scene_Add(p_scene, CANVAS_ENTITY_TEXT_AREA, (Vector2){-550.0f, -350.0f});
    Canvas_Scene_Add(p_scene, CANVAS_ENTITY_DROPDOWN, (Vector2){-230.0f, -350.0f});
    Canvas_Scene_Add(p_scene, CANVAS_ENTITY_ACCORDION, (Vector2){30.0f, -350.0f});
    Canvas_Scene_Add(p_scene, CANVAS_ENTITY_CARD, (Vector2){360.0f, -350.0f});

    Canvas_Scene_Add(p_scene, CANVAS_ENTITY_CALENDAR, (Vector2){-550.0f, -130.0f});
    Canvas_Scene_Add(p_scene, CANVAS_ENTITY_TABLE, (Vector2){-230.0f, -130.0f});
    Canvas_Scene_Add(p_scene, CANVAS_ENTITY_NOTIFICATION, (Vector2){220.0f, -130.0f});
    Canvas_Scene_Add(p_scene, CANVAS_ENTITY_NOTIFICATION, (Vector2){220.0f, -20.0f});
    Canvas_Scene_Add(p_scene, CANVAS_ENTITY_NOTIFICATION, (Vector2){220.0f, 90.0f});
    Canvas_Scene_Add(p_scene, CANVAS_ENTITY_SCROLL_AREA, (Vector2){610.0f, -130.0f});

    Canvas_Scene_Add(p_scene, CANVAS_ENTITY_IMAGE, (Vector2){-550.0f, 220.0f});
    Canvas_Scene_Add(p_scene, CANVAS_ENTITY_WEB_CONTENT, (Vector2){-150.0f, 120.0f});
}

void Canvas_Scene_Add_Browser_Grid(Canvas_Scene *p_scene)
{
    Canvas_Scene_Clear(p_scene);
    for (int32 row = 0; row < 2; row++) {
        for (int32 column = 0; column < 5; column++) {
            Vector2 position = {
                -614.0f + (float)column * 248.0f,
                -182.0f + (float)row * 188.0f,
            };
            if (!Canvas_Scene_Add(p_scene, CANVAS_ENTITY_WEB_CONTENT, position)) return;
            Canvas_Entity *p_entity =
                &p_scene->p_entities[Dowa_Array_Length(p_scene->p_entities) - 1];
            p_entity->size = (Vector2){236.0f, 176.0f};
        }
    }
}

static float Canvas_Distance_To_Segment(Vector2 point, Vector2 start, Vector2 end)
{
    Vector2 segment = {end.x - start.x, end.y - start.y};
    Vector2 relative = {point.x - start.x, point.y - start.y};
    float length_squared = segment.x * segment.x + segment.y * segment.y;
    if (length_squared <= 0.0001f) {
        return sqrtf(relative.x * relative.x + relative.y * relative.y);
    }

    float projection = (relative.x * segment.x + relative.y * segment.y) / length_squared;
    projection = Canvas_Clamp(projection, 0.0f, 1.0f);
    Vector2 closest = {
        start.x + segment.x * projection,
        start.y + segment.y * projection,
    };
    float dx = point.x - closest.x;
    float dy = point.y - closest.y;
    return sqrtf(dx * dx + dy * dy);
}

static boolean Canvas_Entity_Contains(
    const Canvas_Entity *p_entity,
    Vector2 point,
    float zoom)
{
    switch (p_entity->type) {
        case CANVAS_ENTITY_RECTANGLE:
            return CheckCollisionPointRec(
                point,
                (Rectangle){
                    p_entity->position.x,
                    p_entity->position.y,
                    p_entity->size.x,
                    p_entity->size.y,
                }) ? TRUE : FALSE;
        case CANVAS_ENTITY_CIRCLE: {
            float dx = point.x - p_entity->position.x;
            float dy = point.y - p_entity->position.y;
            return (dx * dx + dy * dy <= p_entity->size.x * p_entity->size.x) ? TRUE : FALSE;
        }
        case CANVAS_ENTITY_LINE: {
            Vector2 end = {
                p_entity->position.x + p_entity->size.x,
                p_entity->position.y + p_entity->size.y,
            };
            return Canvas_Distance_To_Segment(point, p_entity->position, end) <= 10.0f / zoom;
        }
        case CANVAS_ENTITY_TEXT:
        case CANVAS_ENTITY_BUTTON:
        case CANVAS_ENTITY_TEXT_AREA:
        case CANVAS_ENTITY_DROPDOWN:
        case CANVAS_ENTITY_ACCORDION:
        case CANVAS_ENTITY_CARD:
        case CANVAS_ENTITY_CALENDAR:
        case CANVAS_ENTITY_SWITCH:
        case CANVAS_ENTITY_TABLE:
        case CANVAS_ENTITY_SCROLL_AREA:
        case CANVAS_ENTITY_IMAGE: {
            float height = p_entity->size.y;
            if (p_entity->type == CANVAS_ENTITY_DROPDOWN && p_entity->active) {
                height += 8.0f + 3.0f * 40.0f;
            }
            return CheckCollisionPointRec(
                point,
                (Rectangle){
                    p_entity->position.x,
                    p_entity->position.y,
                    p_entity->size.x,
                    height,
                }) ? TRUE : FALSE;
        }
        case CANVAS_ENTITY_NOTIFICATION:
            if (p_entity->animation_amount <= 0.01f) return FALSE;
            return CheckCollisionPointRec(
                point,
                (Rectangle){
                    p_entity->position.x,
                    p_entity->position.y,
                    p_entity->size.x,
                    p_entity->size.y,
                }) ? TRUE : FALSE;
        case CANVAS_ENTITY_WEB_CONTENT: {
            float frame_inset = CANVAS_WEB_FRAME_INSET / zoom;
            float resize_handle = CANVAS_WEB_RESIZE_HANDLE / zoom;
            Rectangle outer = {
                p_entity->position.x,
                p_entity->position.y,
                p_entity->size.x,
                p_entity->size.y,
            };
            Rectangle content = {
                outer.x + frame_inset,
                outer.y + frame_inset,
                outer.width - frame_inset - resize_handle,
                outer.height - frame_inset - resize_handle,
            };
            return CheckCollisionPointRec(point, outer) &&
                !CheckCollisionPointRec(point, content);
        }
        default:
            return FALSE;
    }
}

static boolean Canvas_Web_Resize_Handle_Contains(
    const Canvas_Entity *p_entity,
    Vector2 point,
    float zoom)
{
    if (p_entity->type != CANVAS_ENTITY_WEB_CONTENT &&
        p_entity->type != CANVAS_ENTITY_IMAGE) {
        return FALSE;
    }
    float handle_size = CANVAS_WEB_RESIZE_HANDLE / zoom;
    return CheckCollisionPointRec(
        point,
        (Rectangle){
            p_entity->position.x + p_entity->size.x - handle_size,
            p_entity->position.y + p_entity->size.y - handle_size,
            handle_size,
            handle_size,
        }) ? TRUE : FALSE;
}

int32 Canvas_Scene_Pick(
    const Canvas_Scene *p_scene,
    Vector2 world_pointer,
    float zoom)
{
    for (int32 index = (int32)Dowa_Array_Length(p_scene->p_entities) - 1; index >= 0; index--) {
        const Canvas_Entity *p_entity = &p_scene->p_entities[index];
        if (p_entity->type == CANVAS_ENTITY_DROPDOWN &&
            p_entity->active &&
            Canvas_Entity_Contains(p_entity, world_pointer, zoom)) {
            return index;
        }
    }

    for (int32 index = (int32)Dowa_Array_Length(p_scene->p_entities) - 1; index >= 0; index--) {
        if (Canvas_Entity_Contains(&p_scene->p_entities[index], world_pointer, zoom)) return index;
    }
    return -1;
}

static void Canvas_Scene_Activate(Canvas_Scene *p_scene, int32 index, Vector2 world_pointer)
{
    if (index < 0 || index >= (int32)Dowa_Array_Length(p_scene->p_entities)) return;

    Canvas_Entity *p_entity = &p_scene->p_entities[index];
    switch (p_entity->type) {
        case CANVAS_ENTITY_BUTTON:
            p_entity->active = !p_entity->active;
            break;
        case CANVAS_ENTITY_TEXT_AREA:
            p_entity->active = TRUE;
            break;
        case CANVAS_ENTITY_DROPDOWN:
            if (world_pointer.y <= p_entity->position.y + p_entity->size.y) {
                p_entity->active = !p_entity->active;
            } else if (p_entity->active) {
                float options_y = p_entity->position.y + p_entity->size.y + 8.0f;
                int32 option = (int32)((world_pointer.y - options_y) / 40.0f);
                if (option >= 0 && option < 3) p_entity->value = option;
                p_entity->active = FALSE;
            }
            break;
        case CANVAS_ENTITY_ACCORDION:
            p_entity->active = !p_entity->active;
            break;
        case CANVAS_ENTITY_CARD:
        case CANVAS_ENTITY_SWITCH:
            p_entity->active = !p_entity->active;
            break;
        case CANVAS_ENTITY_NOTIFICATION: {
            float local_x =
                (world_pointer.x - p_entity->position.x) / p_entity->size.x;
            if (local_x >= 0.68f) p_entity->active = FALSE;
            break;
        }
        case CANVAS_ENTITY_CALENDAR: {
            float grid_x = p_entity->position.x + 14.0f;
            float grid_y = p_entity->position.y + 82.0f;
            if (world_pointer.x < grid_x ||
                world_pointer.y < grid_y ||
                world_pointer.x >= grid_x + 252.0f ||
                world_pointer.y >= grid_y + 192.0f) {
                break;
            }
            int32 column = (int32)((world_pointer.x - grid_x) / 36.0f);
            int32 row = (int32)((world_pointer.y - grid_y) / 32.0f);
            int32 day = row * 7 + column - 6 + 1;
            if (column >= 0 && column < 7 &&
                row >= 0 && row < 6 &&
                day >= 1 && day <= 31) {
                p_entity->value = day;
            }
            break;
        }
        case CANVAS_ENTITY_TABLE: {
            float rows_y = p_entity->position.y + 62.0f;
            int32 row = (int32)((world_pointer.y - rows_y) / 38.0f);
            if (world_pointer.y >= rows_y && row >= 0 && row < 4) {
                p_entity->value = row;
            }
            break;
        }
        default:
            break;
    }
}

static void Canvas_Scene_Update_Text_Input(Canvas_Scene *p_scene)
{
    if (p_scene->selected_index < 0 ||
        p_scene->selected_index >= (int32)Dowa_Array_Length(p_scene->p_entities)) return;

    Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index];
    if (p_entity->type != CANVAS_ENTITY_TEXT_AREA || !p_entity->active) return;

    size_t length = strlen(p_entity->text);
    int32 codepoint = GetCharPressed();
    while (codepoint > 0) {
        if (codepoint >= 32 && codepoint <= 126 && length + 1 < sizeof(p_entity->text)) {
            p_entity->text[length++] = (char)codepoint;
            p_entity->text[length] = '\0';
        }
        codepoint = GetCharPressed();
    }

    if (IsKeyPressed(KEY_ENTER) && length + 1 < sizeof(p_entity->text)) {
        p_entity->text[length++] = '\n';
        p_entity->text[length] = '\0';
    }
    if (IsKeyPressed(KEY_BACKSPACE) && length > 0) {
        p_entity->text[length - 1] = '\0';
    }
}

static void Canvas_Entity_Capture_Pin(
    Canvas_Entity *p_entity,
    const Canvas_Camera *p_camera)
{
    p_entity->pinned_screen_position =
        Canvas_Camera_World_To_Screen(p_camera, p_entity->position);
    p_entity->pinned_screen_size = (Vector2){
        p_entity->size.x * p_camera->zoom,
        p_entity->size.y * p_camera->zoom,
    };
}

static void Canvas_Scene_Update_Animations(
    Canvas_Scene *p_scene,
    const Canvas_Camera *p_camera)
{
    for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
        Canvas_Entity *p_entity = &p_scene->p_entities[index];
        if (p_entity->type != CANVAS_ENTITY_ACCORDION &&
            p_entity->type != CANVAS_ENTITY_SWITCH) {
            continue;
        }

        float target = p_entity->active ? 1.0f : 0.0f;
        float speed = p_entity->type == CANVAS_ENTITY_SWITCH ? 7.0f : 4.5f;
        float step = GetFrameTime() * speed;
        if (p_entity->animation_amount < target) {
            p_entity->animation_amount = fminf(
                target,
                p_entity->animation_amount + step);
        } else if (p_entity->animation_amount > target) {
            p_entity->animation_amount = fmaxf(
                target,
                p_entity->animation_amount - step);
        }
        float amount = p_entity->animation_amount;
        float eased = amount * amount * (3.0f - 2.0f * amount);
        if (p_entity->type == CANVAS_ENTITY_SWITCH) continue;
        if (p_entity->pinned) {
            float pinned_scale = p_entity->pinned_screen_size.x / 300.0f;
            p_entity->pinned_screen_size.y =
                (56.0f + 108.0f * eased) * pinned_scale;
            p_entity->size.y =
                p_entity->pinned_screen_size.y / p_camera->zoom;
        } else {
            p_entity->size.y = 56.0f + 108.0f * eased;
        }
    }
}

boolean Canvas_Scene_Update_Interaction(
    Canvas_Scene *p_scene,
    const Canvas_Camera *p_camera,
    boolean block_pointer_input)
{
    Canvas_Scene_Update_Text_Input(p_scene);
    Canvas_Scene_Update_Animations(p_scene, p_camera);

    if (block_pointer_input && p_scene->pressed_index < 0) {
        return FALSE;
    }

    Vector2 world_pointer = Canvas_Camera_Screen_To_World(p_camera, GetMousePosition());
    p_scene->hover_index = block_pointer_input ?
        -1 :
        Canvas_Scene_Pick(p_scene, world_pointer, p_camera->zoom);
    boolean consumed_scroll = FALSE;
    if (p_scene->hover_index >= 0) {
        Canvas_Entity *p_hovered = &p_scene->p_entities[p_scene->hover_index];
        if (p_hovered->type == CANVAS_ENTITY_SCROLL_AREA) {
            float wheel = GetMouseWheelMove();
            if (wheel != 0.0f) {
                p_hovered->value = (int32)Canvas_Clamp(
                    (float)p_hovered->value - wheel * 28.0f,
                    0.0f,
                    240.0f);
                consumed_scroll = TRUE;
            }
        }
    }

    float animation_step = Canvas_Clamp(GetFrameTime() * 12.0f, 0.0f, 1.0f);
    for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
        float target = ((int32)index == p_scene->hover_index) ? 1.0f : 0.0f;
        Canvas_Entity *p_entity = &p_scene->p_entities[index];
        p_entity->hover_amount += (target - p_entity->hover_amount) * animation_step;
    }

    if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && !IsKeyDown(KEY_SPACE)) {
        p_scene->selected_index = Canvas_Scene_Pick(p_scene, world_pointer, p_camera->zoom);
        for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
            if ((int32)index != p_scene->selected_index &&
                p_scene->p_entities[index].type == CANVAS_ENTITY_TEXT_AREA) {
                p_scene->p_entities[index].active = FALSE;
            }
            if ((int32)index != p_scene->selected_index &&
                p_scene->p_entities[index].type == CANVAS_ENTITY_DROPDOWN) {
                p_scene->p_entities[index].active = FALSE;
            }
        }

        if (p_scene->selected_index >= 0) {
            Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index];
            p_scene->pressed_index = p_scene->selected_index;
            p_scene->dragging = FALSE;
            p_scene->resizing = Canvas_Web_Resize_Handle_Contains(
                p_entity,
                world_pointer,
                p_camera->zoom);
            p_scene->pointer_start = world_pointer;
            p_scene->entity_start = p_entity->position;
            p_scene->entity_size_start = p_entity->size;
        }
    }

    if (p_scene->pressed_index >= 0 && IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
        Vector2 delta = {
            world_pointer.x - p_scene->pointer_start.x,
            world_pointer.y - p_scene->pointer_start.y,
        };
        float screen_distance_squared =
            (delta.x * p_camera->zoom) * (delta.x * p_camera->zoom) +
            (delta.y * p_camera->zoom) * (delta.y * p_camera->zoom);
        if (screen_distance_squared >= 16.0f) p_scene->dragging = TRUE;

        if (p_scene->dragging) {
            Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->pressed_index];
            if (p_entity->type == CANVAS_ENTITY_NOTIFICATION) {
                p_scene->dragging = FALSE;
            } else if (p_scene->resizing) {
                p_entity->size = (Vector2){
                    fmaxf(CANVAS_WEB_MIN_WIDTH, p_scene->entity_size_start.x + delta.x),
                    fmaxf(CANVAS_WEB_MIN_HEIGHT, p_scene->entity_size_start.y + delta.y),
                };
            } else {
                p_entity->position = (Vector2){
                    p_scene->entity_start.x + delta.x,
                    p_scene->entity_start.y + delta.y,
                };
            }
            if (p_entity->pinned) Canvas_Entity_Capture_Pin(p_entity, p_camera);
        }
    }

    if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) {
        if (p_scene->pressed_index >= 0 && !p_scene->dragging) {
            Canvas_Scene_Activate(p_scene, p_scene->pressed_index, world_pointer);
            Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->pressed_index];
            if (p_entity->pinned) Canvas_Entity_Capture_Pin(p_entity, p_camera);
        }
        p_scene->pressed_index = -1;
        p_scene->dragging = FALSE;
        p_scene->resizing = FALSE;
    }
    return p_scene->pressed_index >= 0 || consumed_scroll;
}

boolean Canvas_Scene_Is_Text_Editing(const Canvas_Scene *p_scene)
{
    if (p_scene->selected_index < 0 ||
        p_scene->selected_index >= (int32)Dowa_Array_Length(p_scene->p_entities)) {
        return FALSE;
    }

    const Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index];
    return (p_entity->type == CANVAS_ENTITY_TEXT_AREA && p_entity->active) ? TRUE : FALSE;
}

void Canvas_Scene_Toggle_Selected_Pin(
    Canvas_Scene *p_scene,
    const Canvas_Camera *p_camera)
{
    if (p_scene->selected_index < 0 ||
        p_scene->selected_index >= (int32)Dowa_Array_Length(p_scene->p_entities)) {
        return;
    }

    Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index];
    if (p_entity->type == CANVAS_ENTITY_NOTIFICATION) return;
    p_entity->pinned = !p_entity->pinned;
    if (p_entity->pinned) Canvas_Entity_Capture_Pin(p_entity, p_camera);
}

void Canvas_Scene_Update_Notification_Stack(
    Canvas_Scene *p_scene,
    const Canvas_Camera *p_camera)
{
    const float width = 384.0f;
    const float height = 82.0f;
    const float margin = 24.0f;
    const float gap = 12.0f;
    float delta = GetFrameTime();
    float follow = 1.0f - expf(-delta * 16.0f);
    Vector2 mouse = GetMousePosition();
    boolean hovered = FALSE;
    int32 active_count = 0;

    for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
        Canvas_Entity *p_entity = &p_scene->p_entities[index];
        if (p_entity->type != CANVAS_ENTITY_NOTIFICATION) continue;
        float visibility_target = p_entity->active ? 1.0f : 0.0f;
        if (p_entity->animation_amount < visibility_target) {
            p_entity->animation_amount = fminf(
                visibility_target,
                p_entity->animation_amount + delta * 7.0f);
        } else if (p_entity->animation_amount > visibility_target) {
            p_entity->animation_amount = fmaxf(
                visibility_target,
                p_entity->animation_amount - delta * 7.0f);
        }
        if (p_entity->active) active_count++;
        if (p_entity->screen_space_initialized &&
            p_entity->animation_amount > 0.01f &&
            CheckCollisionPointRec(
                mouse,
                (Rectangle){
                    p_entity->pinned_screen_position.x,
                    p_entity->pinned_screen_position.y,
                    p_entity->pinned_screen_size.x,
                    p_entity->pinned_screen_size.y,
                })) {
            hovered = TRUE;
        }
    }

    if (!hovered &&
        active_count > 1 &&
        p_scene->notification_stack_amount > 0.08f) {
        float right = (float)p_camera->viewport_width - margin;
        float bottom = (float)p_camera->viewport_height - margin;
        float top = bottom - (float)active_count * height -
            (float)(active_count - 1) * gap;
        hovered = CheckCollisionPointRec(
            mouse,
            (Rectangle){right - width, top, width, bottom - top}) ? TRUE : FALSE;
    }

    float stack_target = hovered ? 1.0f : 0.0f;
    p_scene->notification_stack_amount +=
        (stack_target - p_scene->notification_stack_amount) * follow;
    float amount = p_scene->notification_stack_amount;
    float eased = amount * amount * (3.0f - 2.0f * amount);
    int32 rank = 0;

    for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
        Canvas_Entity *p_entity = &p_scene->p_entities[index];
        if (p_entity->type != CANVAS_ENTITY_NOTIFICATION ||
            p_entity->animation_amount <= 0.01f) {
            continue;
        }

        Vector2 target_position = p_entity->pinned_screen_position;
        Vector2 target_size = {width, height};
        if (p_entity->active) {
            int32 depth = active_count - rank - 1;
            Vector2 collapsed_position = {
                (float)p_camera->viewport_width - margin - width + (float)depth * 8.0f,
                (float)p_camera->viewport_height - margin - height - (float)depth * 8.0f,
            };
            Vector2 expanded_position = {
                (float)p_camera->viewport_width - margin - width,
                (float)p_camera->viewport_height - margin - height -
                    (float)depth * (height + gap),
            };
            target_position = (Vector2){
                collapsed_position.x +
                    (expanded_position.x - collapsed_position.x) * eased,
                collapsed_position.y +
                    (expanded_position.y - collapsed_position.y) * eased,
            };
            target_size.x = width - (float)depth * 12.0f * (1.0f - eased);
            rank++;
        } else {
            target_position.x = (float)p_camera->viewport_width + 20.0f;
        }

        if (!p_entity->screen_space_initialized) {
            p_entity->pinned_screen_position = target_position;
            p_entity->pinned_screen_size = target_size;
            p_entity->screen_space_initialized = TRUE;
        } else {
            p_entity->pinned_screen_position.x +=
                (target_position.x - p_entity->pinned_screen_position.x) * follow;
            p_entity->pinned_screen_position.y +=
                (target_position.y - p_entity->pinned_screen_position.y) * follow;
            p_entity->pinned_screen_size.x +=
                (target_size.x - p_entity->pinned_screen_size.x) * follow;
            p_entity->pinned_screen_size.y +=
                (target_size.y - p_entity->pinned_screen_size.y) * follow;
        }
        p_entity->position = Canvas_Camera_Screen_To_World(
            p_camera,
            p_entity->pinned_screen_position);
        p_entity->size = (Vector2){
            p_entity->pinned_screen_size.x / p_camera->zoom,
            p_entity->pinned_screen_size.y / p_camera->zoom,
        };
    }
}

void Canvas_Scene_Sync_Pinned(
    Canvas_Scene *p_scene,
    const Canvas_Camera *p_camera)
{
    for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
        Canvas_Entity *p_entity = &p_scene->p_entities[index];
        if (!p_entity->pinned) continue;

        Vector2 min_offset = {0.0f, 0.0f};
        Vector2 max_offset = p_entity->pinned_screen_size;
        if (p_entity->type == CANVAS_ENTITY_CIRCLE) {
            min_offset = (Vector2){
                -p_entity->pinned_screen_size.x,
                -p_entity->pinned_screen_size.x,
            };
            max_offset = (Vector2){
                p_entity->pinned_screen_size.x,
                p_entity->pinned_screen_size.x,
            };
        } else if (p_entity->type == CANVAS_ENTITY_LINE) {
            min_offset = (Vector2){
                fminf(0.0f, p_entity->pinned_screen_size.x),
                fminf(0.0f, p_entity->pinned_screen_size.y),
            };
            max_offset = (Vector2){
                fmaxf(0.0f, p_entity->pinned_screen_size.x),
                fmaxf(0.0f, p_entity->pinned_screen_size.y),
            };
        }
        float min_x = -min_offset.x;
        float min_y = -min_offset.y;
        float max_x = fmaxf(
            min_x,
            (float)p_camera->viewport_width - max_offset.x);
        float max_y = fmaxf(
            min_y,
            (float)p_camera->viewport_height - max_offset.y);
        p_entity->pinned_screen_position.x = Canvas_Clamp(
            p_entity->pinned_screen_position.x,
            min_x,
            max_x);
        p_entity->pinned_screen_position.y = Canvas_Clamp(
            p_entity->pinned_screen_position.y,
            min_y,
            max_y);
        p_entity->position = Canvas_Camera_Screen_To_World(
            p_camera,
            p_entity->pinned_screen_position);
        p_entity->size = (Vector2){
            p_entity->pinned_screen_size.x / p_camera->zoom,
            p_entity->pinned_screen_size.y / p_camera->zoom,
        };
    }
}

void Canvas_Update_Camera_Input(
    Canvas_Camera *p_camera,
    boolean block_pointer_input,
    boolean block_keyboard_input)
{
    boolean keyboard_pan = IsKeyDown(KEY_SPACE);
    boolean pointer_pan = IsMouseButtonDown(MOUSE_BUTTON_MIDDLE) ||
        (keyboard_pan && IsMouseButtonDown(MOUSE_BUTTON_LEFT));

    if (pointer_pan && !block_pointer_input) {
        Canvas_Camera_Pan(p_camera, GetMouseDelta());
    }

    if (!block_pointer_input) {
        float wheel = GetMouseWheelMove();
        if (wheel != 0.0f) {
            Canvas_Camera_Zoom_At(
                p_camera,
                GetMousePosition(),
                powf(1.15f, wheel));
        }
    }

    if (!block_keyboard_input) {
        Vector2 keyboard_delta = {0.0f, 0.0f};
        float speed = 600.0f * GetFrameTime();
        if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT)) keyboard_delta.x += speed;
        if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT)) keyboard_delta.x -= speed;
        if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP)) keyboard_delta.y += speed;
        if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN)) keyboard_delta.y -= speed;
        Canvas_Camera_Pan(p_camera, keyboard_delta);
    }
}

static void Canvas_Draw_Grid(
    const Canvas_Camera *p_camera,
    const Canvas_Theme *p_theme)
{
    Rectangle bounds = Canvas_Camera_Viewport_Bounds(p_camera);
    float spacing = Canvas_Grid_Spacing(p_camera->zoom);
    int32 first_x = (int32)floorf(bounds.x / spacing);
    int32 last_x = (int32)ceilf((bounds.x + bounds.width) / spacing);
    int32 first_y = (int32)floorf(bounds.y / spacing);
    int32 last_y = (int32)ceilf((bounds.y + bounds.height) / spacing);
    Color minor = Fade(p_theme->border, 0.36f);
    Color major = Fade(p_theme->border, 0.65f);
    Color axis = p_theme->text_muted;

    for (int32 index = first_x; index <= last_x; index++) {
        float x = (float)index * spacing;
        Color color = (index == 0) ? axis : ((index % 4 == 0) ? major : minor);
        DrawLineV(
            (Vector2){x, bounds.y},
            (Vector2){x, bounds.y + bounds.height},
            color);
    }

    for (int32 index = first_y; index <= last_y; index++) {
        float y = (float)index * spacing;
        Color color = (index == 0) ? axis : ((index % 4 == 0) ? major : minor);
        DrawLineV(
            (Vector2){bounds.x, y},
            (Vector2){bounds.x + bounds.width, y},
            color);
    }
}

static void Canvas_Draw_Selection(
    const Canvas_Entity *p_entity,
    float zoom)
{
    float line = 2.0f / zoom;
    float padding = 7.0f / zoom;
    Color selection = {0, 122, 255, 230};

    switch (p_entity->type) {
        case CANVAS_ENTITY_RECTANGLE:
            DrawRectangleRoundedLinesEx(
                (Rectangle){
                    p_entity->position.x - padding,
                    p_entity->position.y - padding,
                    p_entity->size.x + padding * 2.0f,
                    p_entity->size.y + padding * 2.0f,
                },
                0.14f,
                12,
                line,
                selection);
            break;
        case CANVAS_ENTITY_CIRCLE:
            DrawCircleLinesV(
                p_entity->position,
                p_entity->size.x + padding,
                selection);
            break;
        case CANVAS_ENTITY_LINE:
            DrawLineEx(
                p_entity->position,
                (Vector2){
                    p_entity->position.x + p_entity->size.x,
                    p_entity->position.y + p_entity->size.y,
                },
                10.0f / zoom,
                Fade(selection, 0.35f));
            break;
        case CANVAS_ENTITY_TEXT:
        case CANVAS_ENTITY_BUTTON:
        case CANVAS_ENTITY_TEXT_AREA:
        case CANVAS_ENTITY_DROPDOWN:
        case CANVAS_ENTITY_ACCORDION:
        case CANVAS_ENTITY_CARD:
        case CANVAS_ENTITY_CALENDAR:
        case CANVAS_ENTITY_SWITCH:
        case CANVAS_ENTITY_TABLE:
        case CANVAS_ENTITY_NOTIFICATION:
        case CANVAS_ENTITY_SCROLL_AREA:
        case CANVAS_ENTITY_IMAGE: {
            DrawRectangleRoundedLinesEx(
                (Rectangle){
                    p_entity->position.x - padding,
                    p_entity->position.y - padding,
                    p_entity->size.x + padding * 2.0f,
                    p_entity->size.y + padding * 2.0f,
                },
                0.18f,
                10,
                line,
                selection);
            break;
        }
        case CANVAS_ENTITY_WEB_CONTENT:
            DrawRectangleRoundedLinesEx(
                (Rectangle){
                    p_entity->position.x - padding,
                    p_entity->position.y - padding,
                    p_entity->size.x + padding * 2.0f,
                    p_entity->size.y + padding * 2.0f,
                },
                0.04f,
                10,
                line,
                selection);
            DrawRectangleRec(
                (Rectangle){
                    p_entity->position.x + p_entity->size.x -
                        CANVAS_WEB_RESIZE_HANDLE / zoom,
                    p_entity->position.y + p_entity->size.y -
                        CANVAS_WEB_RESIZE_HANDLE / zoom,
                    CANVAS_WEB_RESIZE_HANDLE / zoom,
                    CANVAS_WEB_RESIZE_HANDLE / zoom,
                },
                Fade(selection, 0.18f));
            DrawLineEx(
                (Vector2){
                    p_entity->position.x + p_entity->size.x - 11.0f / zoom,
                    p_entity->position.y + p_entity->size.y - 4.0f / zoom,
                },
                (Vector2){
                    p_entity->position.x + p_entity->size.x - 4.0f / zoom,
                    p_entity->position.y + p_entity->size.y - 11.0f / zoom,
                },
                1.5f / zoom,
                selection);
            break;
        default:
            break;
    }

}

static void Canvas_Draw_Entity(
    const Canvas_Entity *p_entity,
    Font font,
    float zoom,
    boolean selected,
    boolean pressed,
    const Canvas_Theme *p_theme)
{
    float shadow_offset = 6.0f / zoom;
    Color shadow = p_theme->shadow;
    static const char *calendar_weekdays[] = {
        "S", "M", "T", "W", "T", "F", "S",
    };

    switch (p_entity->type) {
        case CANVAS_ENTITY_RECTANGLE:
            Canvas_Theme_Draw_Shadow(
                p_theme,
                (Rectangle){
                    p_entity->position.x,
                    p_entity->position.y,
                    p_entity->size.x,
                    p_entity->size.y,
                },
                0.14f,
                12,
                zoom,
                1.0f);
            DrawRectangleRounded(
                (Rectangle){
                    p_entity->position.x,
                    p_entity->position.y,
                    p_entity->size.x,
                    p_entity->size.y,
                },
                0.14f,
                12,
                Fade(p_entity->color, 0.92f));
            DrawRectangleRoundedLinesEx(
                (Rectangle){
                    p_entity->position.x,
                    p_entity->position.y,
                    p_entity->size.x,
                    p_entity->size.y,
                },
                0.14f,
                12,
                1.0f / zoom,
                Fade(BLACK, 0.08f));
            break;
        case CANVAS_ENTITY_CIRCLE:
            DrawCircleV(
                (Vector2){
                    p_entity->position.x + shadow_offset,
                    p_entity->position.y + shadow_offset,
                },
                p_entity->size.x,
                shadow);
            DrawCircleV(p_entity->position, p_entity->size.x, p_entity->color);
            break;
        case CANVAS_ENTITY_LINE:
            DrawLineEx(
                p_entity->position,
                (Vector2){
                    p_entity->position.x + p_entity->size.x,
                    p_entity->position.y + p_entity->size.y,
                },
                5.0f,
                p_entity->color);
            DrawCircleV(p_entity->position, 2.5f, p_entity->color);
            DrawCircleV(
                (Vector2){
                    p_entity->position.x + p_entity->size.x,
                    p_entity->position.y + p_entity->size.y,
                },
                2.5f,
                p_entity->color);
            break;
        case CANVAS_ENTITY_TEXT:
            DrawTextEx(
                font,
                p_entity->text,
                p_entity->position,
                28.0f,
                0.5f,
                p_theme->text);
            break;
        case CANVAS_ENTITY_BUTTON: {
            float hover = p_entity->hover_amount;
            float grow = pressed ? -1.0f : hover * 2.0f;
            float lift = pressed ? 0.0f : hover * 2.0f;
            Rectangle bounds = {
                p_entity->position.x - grow,
                p_entity->position.y - grow - lift,
                p_entity->size.x + grow * 2.0f,
                p_entity->size.y + grow * 2.0f,
            };
            Color base_fill = p_entity->active ?
                (Color){35, 99, 235, 255} :
                (Color){17, 24, 39, 255};
            Color hover_fill = p_entity->active ?
                (Color){59, 130, 246, 255} :
                (Color){39, 48, 64, 255};
            Color fill = Canvas_Color_Lerp(base_fill, hover_fill, hover);
            Canvas_Theme_Draw_Shadow(
                p_theme,
                bounds,
                0.46f,
                16,
                zoom,
                1.0f);
            DrawRectangleRounded(bounds, 0.46f, 16, fill);
            Vector2 text_size = MeasureTextEx(font, p_entity->text, 16.0f, 0.1f);
            DrawTextEx(
                font,
                p_entity->text,
                (Vector2){
                    bounds.x + (bounds.width - text_size.x) * 0.5f,
                    bounds.y + (bounds.height - text_size.y) * 0.5f,
                },
                16.0f,
                0.1f,
                WHITE);
            break;
        }
        case CANVAS_ENTITY_TEXT_AREA: {
            Rectangle bounds = {
                p_entity->position.x,
                p_entity->position.y,
                p_entity->size.x,
                p_entity->size.y,
            };
            Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.10f, 14, zoom, 1.0f);
            DrawRectangleRounded(bounds, 0.10f, 14, p_theme->surface);
            DrawRectangleRoundedLinesEx(
                bounds,
                0.10f,
                14,
                (p_entity->active ? 2.0f : 1.0f) / zoom,
                p_entity->active ? p_theme->accent : p_theme->border);
            DrawTextEx(
                font,
                p_entity->text[0] ? p_entity->text : "Write something...",
                (Vector2){bounds.x + 16.0f, bounds.y + 15.0f},
                16.0f,
                0.1f,
                p_entity->text[0] ? p_theme->text : p_theme->text_muted);
            if (p_entity->active) {
                DrawCircleV(
                    (Vector2){bounds.x + bounds.width - 16.0f, bounds.y + 16.0f},
                    3.0f,
                    p_theme->accent);
            }
            break;
        }
        case CANVAS_ENTITY_DROPDOWN: {
            Rectangle bounds = {
                p_entity->position.x,
                p_entity->position.y,
                p_entity->size.x,
                p_entity->size.y,
            };
            Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.30f, 14, zoom, 1.0f);
            DrawRectangleRounded(bounds, 0.30f, 14, p_theme->surface);
            DrawRectangleRoundedLinesEx(
                bounds,
                0.30f,
                14,
                1.0f / zoom,
                p_theme->border);
            DrawTextEx(
                font,
                CANVAS_DROPDOWN_OPTIONS[p_entity->value],
                (Vector2){bounds.x + 16.0f, bounds.y + 14.0f},
                16.0f,
                0.1f,
                p_theme->text);
            Vector2 chevron = {bounds.x + bounds.width - 22.0f, bounds.y + bounds.height * 0.5f};
            DrawTriangle(
                (Vector2){chevron.x - 5.0f, chevron.y - 2.0f},
                (Vector2){chevron.x, chevron.y + 4.0f},
                (Vector2){chevron.x + 5.0f, chevron.y - 2.0f},
                p_theme->text_muted);
            break;
        }
        case CANVAS_ENTITY_ACCORDION: {
            Rectangle bounds = {
                p_entity->position.x,
                p_entity->position.y,
                p_entity->size.x,
                p_entity->size.y,
            };
            Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.10f, 14, zoom, 1.0f);
            DrawRectangleRounded(bounds, 0.10f, 14, p_theme->surface);
            DrawRectangleRoundedLinesEx(
                bounds,
                0.10f,
                14,
                1.0f / zoom,
                p_theme->border);
            DrawTextEx(
                font,
                p_entity->text,
                (Vector2){bounds.x + 16.0f, bounds.y + 18.0f},
                16.0f,
                0.1f,
                p_theme->text);
            float amount = p_entity->animation_amount;
            float angle = amount * PI * 0.5f;
            Vector2 chevron = {bounds.x + bounds.width - 22.0f, bounds.y + 28.0f};
            Vector2 direction = {cosf(angle), sinf(angle)};
            Vector2 normal = {-direction.y, direction.x};
            Vector2 tip = {
                chevron.x + direction.x * 3.0f,
                chevron.y + direction.y * 3.0f,
            };
            DrawLineEx(
                tip,
                (Vector2){
                    chevron.x - direction.x * 3.0f + normal.x * 4.0f,
                    chevron.y - direction.y * 3.0f + normal.y * 4.0f,
                },
                1.5f / zoom,
                p_theme->text_muted);
            DrawLineEx(
                tip,
                (Vector2){
                    chevron.x - direction.x * 3.0f - normal.x * 4.0f,
                    chevron.y - direction.y * 3.0f - normal.y * 4.0f,
                },
                1.5f / zoom,
                p_theme->text_muted);
            if (amount > 0.0f) {
                DrawLineEx(
                    (Vector2){bounds.x, bounds.y + 56.0f},
                    (Vector2){bounds.x + bounds.width, bounds.y + 56.0f},
                    1.0f / zoom,
                    Fade(p_theme->border, amount));
                if (amount > 0.55f) {
                    float body_alpha = Canvas_Clamp(
                        (amount - 0.55f) / 0.45f,
                        0.0f,
                        1.0f);
                    DrawTextEx(
                        font,
                        "Drag, zoom, pin, and combine primitives\nacross one continuous workspace.",
                        (Vector2){bounds.x + 16.0f, bounds.y + 76.0f},
                        14.0f,
                        0.1f,
                        Fade(p_theme->text_muted, body_alpha));
                }
            }
            break;
        }
        case CANVAS_ENTITY_CARD: {
            Rectangle bounds = {
                p_entity->position.x,
                p_entity->position.y,
                p_entity->size.x,
                p_entity->size.y,
            };
            Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.08f, 14, zoom, 1.0f);
            DrawRectangleRounded(bounds, 0.08f, 14, p_theme->surface);
            DrawRectangleRounded(
                (Rectangle){bounds.x + 12.0f, bounds.y + 12.0f,
                    bounds.width - 24.0f, 92.0f},
                0.08f,
                12,
                p_entity->active ?
                    p_theme->accent_soft :
                    Fade(p_entity->color, 0.22f));
            DrawCircleV(
                (Vector2){bounds.x + 44.0f, bounds.y + 58.0f},
                18.0f,
                p_entity->active ? (Color){37, 99, 235, 255} : p_entity->color);
            DrawTextEx(
                font,
                p_entity->text,
                (Vector2){bounds.x + 16.0f, bounds.y + 122.0f},
                20.0f,
                0.1f,
                p_theme->text);
            DrawTextEx(
                font,
                "A flexible surface for tools,\ncontent, and live browser views.",
                (Vector2){bounds.x + 16.0f, bounds.y + 154.0f},
                14.0f,
                0.1f,
                p_theme->text_muted);
            DrawRectangleRoundedLinesEx(
                bounds,
                0.08f,
                14,
                (p_entity->active ? 2.0f : 1.0f) / zoom,
                p_entity->active ?
                    p_theme->accent :
                    p_theme->border);
            break;
        }
        case CANVAS_ENTITY_CALENDAR: {
            Rectangle bounds = {
                p_entity->position.x,
                p_entity->position.y,
                p_entity->size.x,
                p_entity->size.y,
            };
            Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.08f, 14, zoom, 1.0f);
            DrawRectangleRounded(bounds, 0.08f, 14, p_theme->surface);
            DrawRectangleRoundedLinesEx(
                bounds,
                0.08f,
                14,
                1.0f / zoom,
                p_theme->border);
            DrawTextEx(
                font,
                "August 2026",
                (Vector2){bounds.x + 16.0f, bounds.y + 18.0f},
                20.0f,
                0.1f,
                p_theme->text);
            for (int32 column = 0; column < 7; column++) {
                float cell_x = bounds.x + 14.0f + (float)column * 36.0f;
                DrawTextEx(
                    font,
                    calendar_weekdays[column],
                    (Vector2){cell_x + 12.0f, bounds.y + 56.0f},
                    12.0f,
                    0.0f,
                    p_theme->text_muted);
            }
            for (int32 day = 1; day <= 31; day++) {
                int32 cell = day + 6 - 1;
                int32 row = cell / 7;
                int32 column = cell % 7;
                Vector2 center = {
                    bounds.x + 14.0f + (float)column * 36.0f + 18.0f,
                    bounds.y + 82.0f + (float)row * 32.0f + 16.0f,
                };
                if (day == p_entity->value) {
                    DrawCircleV(center, 14.0f, p_theme->accent);
                }
                const char *label = TextFormat("%d", day);
                Vector2 label_size = MeasureTextEx(font, label, 13.0f, 0.0f);
                DrawTextEx(
                    font,
                    label,
                    (Vector2){center.x - label_size.x * 0.5f,
                        center.y - label_size.y * 0.5f},
                    13.0f,
                    0.0f,
                    day == p_entity->value ? WHITE : p_theme->text);
            }
            break;
        }
        case CANVAS_ENTITY_SWITCH: {
            float amount = p_entity->animation_amount;
            float eased = amount * amount * (3.0f - 2.0f * amount);
            Rectangle bounds = {
                p_entity->position.x,
                p_entity->position.y,
                p_entity->size.x,
                p_entity->size.y,
            };
            Color track = Canvas_Color_Lerp(
                p_theme->border,
                p_theme->accent,
                eased);
            if (!p_entity->active) {
                track = Canvas_Color_Lerp(
                    track,
                    p_theme->text_muted,
                    p_entity->hover_amount);
            }
            Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.50f, 18, zoom, 1.0f);
            DrawRectangleRounded(bounds, 0.50f, 18, track);
            float knob_x = bounds.x + 18.0f +
                eased * (bounds.width - 36.0f);
            float knob_radius = pressed ? 12.5f : 14.0f;
            DrawCircleV(
                (Vector2){knob_x, bounds.y + bounds.height * 0.5f},
                knob_radius,
                WHITE);
            break;
        }
        case CANVAS_ENTITY_TABLE: {
            Rectangle bounds = {
                p_entity->position.x,
                p_entity->position.y,
                p_entity->size.x,
                p_entity->size.y,
            };
            Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.06f, 12, zoom, 1.0f);
            DrawRectangleRounded(bounds, 0.06f, 12, p_theme->surface);
            DrawRectangleRoundedLinesEx(
                bounds,
                0.06f,
                12,
                1.0f / zoom,
                p_theme->border);
            DrawTextEx(
                font,
                "Name",
                (Vector2){bounds.x + 16.0f, bounds.y + 22.0f},
                13.0f,
                0.0f,
                p_theme->text_muted);
            DrawTextEx(
                font,
                "Status",
                (Vector2){bounds.x + 292.0f, bounds.y + 22.0f},
                13.0f,
                0.0f,
                p_theme->text_muted);
            DrawLineEx(
                (Vector2){bounds.x, bounds.y + 52.0f},
                (Vector2){bounds.x + bounds.width, bounds.y + 52.0f},
                1.0f / zoom,
                p_theme->border);
            for (int32 row = 0; row < 4; row++) {
                float row_y = bounds.y + 62.0f + (float)row * 38.0f;
                if (row == p_entity->value) {
                    DrawRectangle(
                        (int32)(bounds.x + 8.0f),
                        (int32)(row_y - 4.0f),
                        (int32)(bounds.width - 16.0f),
                        34,
                        p_theme->accent_soft);
                }
                DrawTextEx(
                    font,
                    CANVAS_TABLE_NAMES[row],
                    (Vector2){bounds.x + 16.0f, row_y + 5.0f},
                    14.0f,
                    0.0f,
                    p_theme->text);
                DrawTextEx(
                    font,
                    CANVAS_TABLE_STATUSES[row],
                    (Vector2){bounds.x + 292.0f, row_y + 5.0f},
                    14.0f,
                    0.0f,
                    row == p_entity->value ?
                        p_theme->accent :
                        p_theme->text_muted);
            }
            break;
        }
        case CANVAS_ENTITY_NOTIFICATION: {
            if (p_entity->animation_amount <= 0.01f) break;
            float unit = 1.0f / zoom;
            Rectangle bounds = {
                p_entity->position.x,
                p_entity->position.y,
                p_entity->size.x,
                p_entity->size.y,
            };
            float opacity = p_entity->animation_amount;
            Canvas_Theme_Draw_Shadow(
                p_theme,
                bounds,
                0.20f,
                16,
                zoom,
                opacity);
            DrawRectangleRounded(
                bounds,
                0.20f,
                16,
                Fade(p_theme->toast_surface, opacity));
            DrawRectangleRoundedLinesEx(
                bounds,
                0.20f,
                16,
                1.0f / zoom,
                Fade(p_theme->toast_border, opacity));
            DrawTextEx(
                font,
                p_entity->text,
                (Vector2){bounds.x + 16.0f * unit, bounds.y + 18.0f * unit},
                15.0f * unit,
                0.0f,
                Fade(p_theme->text, opacity));
            DrawTextEx(
                font,
                "Sunday, December 3 at 9:00 AM",
                (Vector2){bounds.x + 16.0f * unit, bounds.y + 45.0f * unit},
                14.0f * unit,
                0.0f,
                Fade(p_theme->text_muted, opacity));
            Rectangle undo = {
                bounds.x + bounds.width - 108.0f * unit,
                bounds.y + 24.0f * unit,
                54.0f * unit,
                34.0f * unit,
            };
            DrawRectangleRounded(
                undo,
                0.26f,
                10,
                Fade(p_theme->surface_muted, opacity));
            DrawRectangleRoundedLinesEx(
                undo,
                0.26f,
                10,
                1.0f / zoom,
                Fade(p_theme->border, opacity));
            DrawTextEx(
                font,
                "Undo",
                (Vector2){undo.x + 12.0f * unit, undo.y + 10.0f * unit},
                13.0f * unit,
                0.0f,
                Fade(p_theme->text, opacity));
            Vector2 close = {
                bounds.x + bounds.width - 24.0f * unit,
                bounds.y + bounds.height * 0.5f,
            };
            DrawLineEx(
                (Vector2){close.x - 4.0f * unit, close.y - 4.0f * unit},
                (Vector2){close.x + 4.0f * unit, close.y + 4.0f * unit},
                1.5f / zoom,
                Fade(p_theme->text_muted, opacity));
            DrawLineEx(
                (Vector2){close.x + 4.0f * unit, close.y - 4.0f * unit},
                (Vector2){close.x - 4.0f * unit, close.y + 4.0f * unit},
                1.5f / zoom,
                Fade(p_theme->text_muted, opacity));
            break;
        }
        case CANVAS_ENTITY_SCROLL_AREA: {
            Rectangle bounds = {
                p_entity->position.x,
                p_entity->position.y,
                p_entity->size.x,
                p_entity->size.y,
            };
            Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.08f, 12, zoom, 1.0f);
            DrawRectangleRounded(bounds, 0.08f, 12, p_theme->surface);
            DrawRectangleRoundedLinesEx(
                bounds,
                0.08f,
                12,
                1.0f / zoom,
                p_theme->border);
            DrawTextEx(
                font,
                "Scrollable area",
                (Vector2){bounds.x + 16.0f, bounds.y + 16.0f},
                18.0f,
                0.0f,
                p_theme->text);
            for (int32 item = 0; item < 10; item++) {
                float item_y =
                    bounds.y + 52.0f + (float)item * 42.0f - (float)p_entity->value;
                if (item_y < bounds.y + 46.0f ||
                    item_y + 34.0f > bounds.y + bounds.height - 12.0f) {
                    continue;
                }
                DrawRectangleRounded(
                    (Rectangle){bounds.x + 14.0f, item_y,
                        bounds.width - 38.0f, 34.0f},
                    0.16f,
                    8,
                    item % 2 == 0 ?
                        p_theme->surface_muted :
                        Canvas_Color_Lerp(p_theme->surface, p_theme->surface_muted, 0.5f));
                DrawTextEx(
                    font,
                    TextFormat("Item %d", item + 1),
                    (Vector2){bounds.x + 26.0f, item_y + 9.0f},
                    13.0f,
                    0.0f,
                    p_theme->text);
            }
            float thumb_y = bounds.y + 52.0f +
                ((float)p_entity->value / 240.0f) * (bounds.height - 106.0f);
            DrawRectangleRounded(
                (Rectangle){bounds.x + bounds.width - 14.0f, thumb_y, 5.0f, 42.0f},
                0.50f,
                6,
                Fade(p_theme->text_muted, 0.82f));
            break;
        }
        case CANVAS_ENTITY_IMAGE: {
            Rectangle bounds = {
                p_entity->position.x,
                p_entity->position.y,
                p_entity->size.x,
                p_entity->size.y,
            };
            Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.04f, 12, zoom, 1.0f);
            DrawRectangleRounded(bounds, 0.04f, 12, p_theme->surface_muted);
            DrawRectangleRoundedLinesEx(
                bounds,
                0.04f,
                12,
                1.0f / zoom,
                p_theme->border);
            DrawTextEx(
                font,
                "Loading image...",
                (Vector2){bounds.x + 16.0f, bounds.y + 16.0f},
                14.0f,
                0.0f,
                p_theme->text_muted);
            break;
        }
        case CANVAS_ENTITY_WEB_CONTENT: {
            Rectangle bounds = {
                p_entity->position.x,
                p_entity->position.y,
                p_entity->size.x,
                p_entity->size.y,
            };
            Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.04f, 14, zoom, 1.0f);
            DrawRectangleRounded(bounds, 0.04f, 14, p_theme->surface_muted);
            DrawRectangleRoundedLinesEx(
                bounds,
                0.04f,
                14,
                1.0f / zoom,
                p_theme->border);
#if !defined(PLATFORM_WEB) && !defined(__EMSCRIPTEN__)
            Vector2 label_size = MeasureTextEx(
                font,
                "Loading browser surface...",
                18.0f,
                0.1f);
            DrawTextEx(
                font,
                "Loading browser surface...",
                (Vector2){
                    bounds.x + (bounds.width - label_size.x) * 0.5f,
                    bounds.y + (bounds.height - label_size.y) * 0.5f,
                },
                18.0f,
                0.1f,
                p_theme->text_muted);
#endif
            break;
        }
        default:
            break;
    }

    if (p_entity->pinned) {
        float radius = 7.0f / zoom;
        Vector2 center = {
            p_entity->position.x + p_entity->size.x - 10.0f / zoom,
            p_entity->position.y + 10.0f / zoom,
        };
        DrawCircleV(center, radius, p_theme->accent);
        DrawLineEx(
            (Vector2){center.x, center.y - 3.5f / zoom},
            (Vector2){center.x, center.y + 4.0f / zoom},
            1.5f / zoom,
            WHITE);
    }
    if (selected && p_entity->type != CANVAS_ENTITY_NOTIFICATION) {
        Canvas_Draw_Selection(p_entity, zoom);
    }
}

static void Canvas_Draw_Dropdown_Menu(
    const Canvas_Entity *p_entity,
    Font font,
    Vector2 world_pointer,
    float zoom,
    const Canvas_Theme *p_theme)
{
    static const char *options[] = {
        "Design",
        "Engineering",
        "Research",
    };
    Rectangle menu = {
        p_entity->position.x,
        p_entity->position.y + p_entity->size.y + 8.0f,
        p_entity->size.x,
        120.0f,
    };
    Canvas_Theme_Draw_Shadow(p_theme, menu, 0.12f, 14, zoom, 1.0f);
    DrawRectangleRounded(menu, 0.12f, 14, p_theme->surface);
    DrawRectangleRoundedLinesEx(
        menu,
        0.12f,
        14,
        1.0f / zoom,
        p_theme->border);

    for (int32 option = 0; option < 3; option++) {
        Rectangle row = {
            menu.x + 5.0f,
            menu.y + 4.0f + (float)option * 38.0f,
            menu.width - 10.0f,
            36.0f,
        };
        boolean hovered = CheckCollisionPointRec(world_pointer, row) ? TRUE : FALSE;
        boolean selected = p_entity->value == option;
        if (hovered || selected) {
            DrawRectangleRounded(
                row,
                0.20f,
                10,
                hovered ? p_theme->accent_soft : p_theme->surface_muted);
        }
        DrawTextEx(
            font,
            options[option],
            (Vector2){row.x + 12.0f, row.y + 9.0f},
            15.0f,
            0.1f,
            selected ? p_theme->accent : p_theme->text);
        if (selected) {
            DrawCircleV(
                (Vector2){row.x + row.width - 14.0f, row.y + row.height * 0.5f},
                3.0f,
                p_theme->accent);
        }
    }
}

void Canvas_Draw_World(
    const Canvas_Camera *p_camera,
    const Canvas_Scene *p_scene,
    Font font,
    const Canvas_Theme *p_theme)
{
    Vector2 world_pointer = Canvas_Camera_Screen_To_World(p_camera, GetMousePosition());
    Camera2D camera = {
        .offset = {
            (float)p_camera->viewport_width * 0.5f,
            (float)p_camera->viewport_height * 0.5f,
        },
        .target = p_camera->target,
        .rotation = 0.0f,
        .zoom = p_camera->zoom,
    };

    BeginMode2D(camera);
    if (p_scene->show_grid) Canvas_Draw_Grid(p_camera, p_theme);
    for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
        Canvas_Draw_Entity(
            &p_scene->p_entities[index],
            font,
            p_camera->zoom,
            (int32)index == p_scene->selected_index,
            (int32)index == p_scene->pressed_index && IsMouseButtonDown(MOUSE_BUTTON_LEFT),
            p_theme);
    }
    for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
        const Canvas_Entity *p_entity = &p_scene->p_entities[index];
        if (p_entity->type == CANVAS_ENTITY_DROPDOWN && p_entity->active) {
            Canvas_Draw_Dropdown_Menu(
                p_entity,
                font,
                world_pointer,
                p_camera->zoom,
                p_theme);
        }
    }
    EndMode2D();
}

const char *Canvas_Entity_Type_Name(Canvas_Entity_Type type)
{
    switch (type) {
        case CANVAS_ENTITY_RECTANGLE: return "Rectangle";
        case CANVAS_ENTITY_CIRCLE: return "Circle";
        case CANVAS_ENTITY_LINE: return "Line";
        case CANVAS_ENTITY_TEXT: return "Text";
        case CANVAS_ENTITY_BUTTON: return "Button";
        case CANVAS_ENTITY_TEXT_AREA: return "Text area";
        case CANVAS_ENTITY_DROPDOWN: return "Dropdown";
        case CANVAS_ENTITY_ACCORDION: return "Accordion";
        case CANVAS_ENTITY_CARD: return "Card";
        case CANVAS_ENTITY_CALENDAR: return "Calendar";
        case CANVAS_ENTITY_SWITCH: return "Switch";
        case CANVAS_ENTITY_TABLE: return "Table";
        case CANVAS_ENTITY_NOTIFICATION: return "Notification";
        case CANVAS_ENTITY_SCROLL_AREA: return "Scrollable area";
        case CANVAS_ENTITY_IMAGE: return "Image";
        case CANVAS_ENTITY_WEB_CONTENT: return "Web content";
        default: return "Unknown";
    }
}