view infinite_canvas/canvas.c @ 278:8d560f50ed4c

Improve infinite canvas interactions and browser chrome Render Lucide icons directly with Raylib, add searchable icon browsing, robust text editing, entity lifecycle animations, z-order-safe input, semantic themes, and animated editable browser controls. Document rendering, pinning, context, and component extension for future agents. Co-authored-by: Copilot <[email protected]> Copilot-Session: f68442b1-fa8f-46a0-9689-81710613bbd4
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 22:16:14 -0700
parents b55c22cff335
children 49e9e591c9bb
line wrap: on
line source

#include "infinite_canvas/canvas.h"
#include "infinite_canvas/generated/lucide_data.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",
};

#define CANVAS_LUCIDE_COLUMNS 10
#define CANVAS_LUCIDE_CARD_WIDTH 128.0f
#define CANVAS_LUCIDE_CARD_HEIGHT 96.0f
#define CANVAS_LUCIDE_GAP 12.0f
#define CANVAS_LUCIDE_HEADER_HEIGHT 88.0f
#define CANVAS_LUCIDE_PADDING 24.0f
#define CANVAS_LUCIDE_SEARCH_WIDTH 360.0f
#define CANVAS_LUCIDE_SEARCH_HEIGHT 42.0f
#define CANVAS_TEXT_AREA_PADDING 16.0f
#define CANVAS_TEXT_AREA_FONT_SIZE 16.0f
#define CANVAS_TEXT_AREA_SPACING 0.1f
#define CANVAS_TEXT_AREA_LINE_HEIGHT 22.0f

typedef struct {
    int32 start;
    int32 end;
    int32 next;
    float width;
} Canvas_Text_Line;

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),
    };
}

static Color Canvas_Color_With_Opacity(Color color, float opacity)
{
    color.a = (uint8)((float)color.a * Canvas_Clamp(opacity, 0.0f, 1.0f));
    return color;
}

static Color Canvas_Color_Fade(Color color, float amount)
{
    return Canvas_Color_With_Opacity(color, amount);
}

static float Canvas_Entity_Visibility(const Canvas_Entity *p_entity)
{
    float amount = Canvas_Clamp(p_entity->visibility_amount, 0.0f, 1.0f);
    return amount * amount * (3.0f - 2.0f * amount);
}

static Canvas_Theme Canvas_Theme_With_Opacity(
    const Canvas_Theme *p_theme,
    float opacity)
{
    Canvas_Theme faded = *p_theme;
    faded.background = Canvas_Color_With_Opacity(faded.background, opacity);
    faded.surface = Canvas_Color_With_Opacity(faded.surface, opacity);
    faded.surface_muted = Canvas_Color_With_Opacity(faded.surface_muted, opacity);
    faded.border = Canvas_Color_With_Opacity(faded.border, opacity);
    faded.text = Canvas_Color_With_Opacity(faded.text, opacity);
    faded.text_muted = Canvas_Color_With_Opacity(faded.text_muted, opacity);
    faded.accent = Canvas_Color_With_Opacity(faded.accent, opacity);
    faded.accent_soft = Canvas_Color_With_Opacity(faded.accent_soft, opacity);
    faded.on_accent = Canvas_Color_With_Opacity(faded.on_accent, opacity);
    faded.selection = Canvas_Color_With_Opacity(faded.selection, opacity);
    faded.entity_outline = Canvas_Color_With_Opacity(faded.entity_outline, opacity);
    faded.button = Canvas_Color_With_Opacity(faded.button, opacity);
    faded.button_hover = Canvas_Color_With_Opacity(faded.button_hover, opacity);
    faded.button_active = Canvas_Color_With_Opacity(faded.button_active, opacity);
    faded.button_active_hover = Canvas_Color_With_Opacity(
        faded.button_active_hover,
        opacity);
    faded.switch_track_off = Canvas_Color_With_Opacity(
        faded.switch_track_off,
        opacity);
    faded.switch_track_off_hover = Canvas_Color_With_Opacity(
        faded.switch_track_off_hover,
        opacity);
    faded.switch_track_on = Canvas_Color_With_Opacity(
        faded.switch_track_on,
        opacity);
    faded.switch_track_on_hover = Canvas_Color_With_Opacity(
        faded.switch_track_on_hover,
        opacity);
    faded.switch_thumb = Canvas_Color_With_Opacity(faded.switch_thumb, opacity);
    faded.pin = Canvas_Color_With_Opacity(faded.pin, opacity);
    faded.danger = Canvas_Color_With_Opacity(faded.danger, opacity);
    faded.shadow = Canvas_Color_With_Opacity(faded.shadow, opacity);
    faded.toast_surface = Canvas_Color_With_Opacity(faded.toast_surface, opacity);
    faded.toast_border = Canvas_Color_With_Opacity(faded.toast_border, opacity);
    return faded;
}

static boolean Canvas_Control_Is_Down(void)
{
    return IsKeyDown(KEY_LEFT_CONTROL) ||
        IsKeyDown(KEY_RIGHT_CONTROL) ||
        IsKeyDown(KEY_LEFT_SUPER) ||
        IsKeyDown(KEY_RIGHT_SUPER);
}

static boolean Canvas_Shift_Is_Down(void);
static boolean Canvas_Key_Activated(int32 key);

static char Canvas_Ascii_Lower(char value)
{
    if (value >= 'A' && value <= 'Z') return (char)(value + ('a' - 'A'));
    return value;
}

static boolean Canvas_Lucide_Name_Matches(
    const char *p_name,
    const char *p_query)
{
    if (!p_query || !p_query[0]) return TRUE;
    for (const char *p_start = p_name; *p_start; p_start++) {
        const char *p_name_cursor = p_start;
        const char *p_query_cursor = p_query;
        while (*p_name_cursor &&
            *p_query_cursor &&
            Canvas_Ascii_Lower(*p_name_cursor) ==
                Canvas_Ascii_Lower(*p_query_cursor)) {
            p_name_cursor++;
            p_query_cursor++;
        }
        if (!*p_query_cursor) return TRUE;
    }
    return FALSE;
}

int32 Canvas_Lucide_Count_Matches(const char *p_query)
{
    int32 count = 0;
    for (int32 index = 0; index < CANVAS_LUCIDE_ICON_COUNT; index++) {
        if (Canvas_Lucide_Name_Matches(
                CANVAS_LUCIDE_ICONS[index].p_name,
                p_query)) {
            count++;
        }
    }
    return count;
}

static Rectangle Canvas_Lucide_Search_Bounds(const Canvas_Entity *p_entity)
{
    return (Rectangle){
        p_entity->position.x + p_entity->size.x -
            CANVAS_LUCIDE_PADDING - CANVAS_LUCIDE_SEARCH_WIDTH,
        p_entity->position.y +
            (CANVAS_LUCIDE_HEADER_HEIGHT - CANVAS_LUCIDE_SEARCH_HEIGHT) * 0.5f,
        CANVAS_LUCIDE_SEARCH_WIDTH,
        CANVAS_LUCIDE_SEARCH_HEIGHT,
    };
}

static float Canvas_Lucide_Content_Height(const char *p_query)
{
    int32 icon_count = Canvas_Lucide_Count_Matches(p_query);
    int32 rows =
        (icon_count + CANVAS_LUCIDE_COLUMNS - 1) /
        CANVAS_LUCIDE_COLUMNS;
    if (rows == 0) {
        return CANVAS_LUCIDE_HEADER_HEIGHT + CANVAS_LUCIDE_PADDING * 2.0f;
    }
    return CANVAS_LUCIDE_HEADER_HEIGHT +
        CANVAS_LUCIDE_PADDING +
        (float)rows * CANVAS_LUCIDE_CARD_HEIGHT +
        (float)(rows - 1) * CANVAS_LUCIDE_GAP +
        CANVAS_LUCIDE_PADDING;
}

static float Canvas_Lucide_Max_Scroll(const Canvas_Entity *p_entity)
{
    return fmaxf(
        0.0f,
        Canvas_Lucide_Content_Height(p_entity->text) - p_entity->size.y);
}

static const Canvas_Lucide_Icon *Canvas_Lucide_Find_Icon(
    const char *p_name)
{
    for (int32 index = 0; index < CANVAS_LUCIDE_ICON_COUNT; index++) {
        if (strcmp(CANVAS_LUCIDE_ICONS[index].p_name, p_name) == 0) {
            return &CANVAS_LUCIDE_ICONS[index];
        }
    }
    return NULL;
}

static void Canvas_Lucide_Draw_Icon(
    const Canvas_Lucide_Icon *p_icon,
    Vector2 origin,
    float scale,
    float line_thickness,
    Color color)
{
    if (!p_icon) return;
    for (uint32 segment_index = p_icon->first_segment;
        segment_index < p_icon->first_segment + p_icon->segment_count;
        segment_index++) {
        const Canvas_Lucide_Segment *p_segment =
            &CANVAS_LUCIDE_SEGMENTS[segment_index];
        DrawLineEx(
            (Vector2){
                origin.x + p_segment->x1 * scale,
                origin.y + p_segment->y1 * scale,
            },
            (Vector2){
                origin.x + p_segment->x2 * scale,
                origin.y + p_segment->y2 * scale,
            },
            line_thickness,
            color);
    }
}

static Rectangle Canvas_Web_Toolbar_Bounds(
    const Canvas_Camera *p_camera,
    const Canvas_Entity *p_entity)
{
    float zoom = p_camera->zoom;
    return (Rectangle){
        p_entity->position.x + CANVAS_WEB_FRAME_INSET / zoom,
        p_entity->position.y + CANVAS_WEB_FRAME_INSET / zoom,
        p_entity->size.x -
            (CANVAS_WEB_FRAME_INSET + CANVAS_WEB_RESIZE_HANDLE) / zoom,
        CANVAS_WEB_TOOLBAR_HEIGHT *
            (p_entity->web_chrome_amount *
                p_entity->web_chrome_amount *
                (3.0f - 2.0f * p_entity->web_chrome_amount)) /
            zoom,
    };
}

static Rectangle Canvas_Web_Toolbar_Button_Bounds(
    Rectangle toolbar,
    float zoom,
    int32 button_index)
{
    return (Rectangle){
        toolbar.x + (6.0f + (float)button_index * 34.0f) / zoom,
        toolbar.y + 7.0f / zoom,
        28.0f / zoom,
        28.0f / zoom,
    };
}

static Rectangle Canvas_Web_Address_Bounds(Rectangle toolbar, float zoom)
{
    return (Rectangle){
        toolbar.x + 76.0f / zoom,
        toolbar.y + 7.0f / zoom,
        toolbar.width - 82.0f / zoom,
        28.0f / zoom,
    };
}

static void Canvas_Web_URL_Selection_Bounds(
    const Canvas_Scene *p_scene,
    int32 *p_start,
    int32 *p_end)
{
    *p_start = p_scene->web_url_cursor;
    *p_end = p_scene->web_url_selection_anchor;
    if (*p_start > *p_end) {
        int32 swap = *p_start;
        *p_start = *p_end;
        *p_end = swap;
    }
}

static void Canvas_Web_URL_Delete_Selection(Canvas_Scene *p_scene)
{
    int32 start = 0;
    int32 end = 0;
    Canvas_Web_URL_Selection_Bounds(p_scene, &start, &end);
    if (start == end) return;
    size_t length = strlen(p_scene->web_url_draft);
    memmove(
        p_scene->web_url_draft + start,
        p_scene->web_url_draft + end,
        length - (size_t)end + 1);
    p_scene->web_url_cursor = start;
    p_scene->web_url_selection_anchor = start;
}

static void Canvas_Web_URL_Insert(Canvas_Scene *p_scene, const char *p_text)
{
    Canvas_Web_URL_Delete_Selection(p_scene);
    size_t length = strlen(p_scene->web_url_draft);
    size_t available = sizeof(p_scene->web_url_draft) - length - 1;
    if (available == 0) return;
    char filtered[CANVAS_ENTITY_TEXT_CAPACITY];
    size_t filtered_length = 0;
    for (const char *p_cursor = p_text;
        *p_cursor && filtered_length < available;
        p_cursor++) {
        if (*p_cursor >= 32 && *p_cursor <= 126) {
            filtered[filtered_length++] = *p_cursor;
        }
    }
    if (filtered_length == 0) return;
    int32 cursor = p_scene->web_url_cursor;
    memmove(
        p_scene->web_url_draft + cursor + filtered_length,
        p_scene->web_url_draft + cursor,
        length - (size_t)cursor + 1);
    memcpy(p_scene->web_url_draft + cursor, filtered, filtered_length);
    p_scene->web_url_cursor += (int32)filtered_length;
    p_scene->web_url_selection_anchor = p_scene->web_url_cursor;
}

static int32 Canvas_Web_URL_Cursor_At_Point(
    Font font,
    const Canvas_Scene *p_scene,
    Rectangle address,
    float zoom,
    Vector2 point)
{
    float font_size = 13.0f / zoom;
    float local_x =
        point.x - address.x - 9.0f / zoom + p_scene->web_url_scroll_x;
    int32 length = (int32)strlen(p_scene->web_url_draft);
    for (int32 cursor = 0; cursor < length; cursor++) {
        char prefix[CANVAS_ENTITY_TEXT_CAPACITY];
        memcpy(prefix, p_scene->web_url_draft, (size_t)cursor + 1);
        prefix[cursor + 1] = '\0';
        float right = MeasureTextEx(font, prefix, font_size, 0.0f).x;
        prefix[cursor] = '\0';
        float left = MeasureTextEx(font, prefix, font_size, 0.0f).x;
        if (local_x < (left + right) * 0.5f) return cursor;
    }
    return length;
}

static void Canvas_Web_URL_Ensure_Cursor_Visible(
    Canvas_Scene *p_scene,
    Font font,
    Rectangle address,
    float zoom)
{
    char prefix[CANVAS_ENTITY_TEXT_CAPACITY];
    memcpy(
        prefix,
        p_scene->web_url_draft,
        (size_t)p_scene->web_url_cursor);
    prefix[p_scene->web_url_cursor] = '\0';
    float cursor_x = MeasureTextEx(font, prefix, 13.0f / zoom, 0.0f).x;
    float visible_width = address.width - 18.0f / zoom;
    if (cursor_x < p_scene->web_url_scroll_x) {
        p_scene->web_url_scroll_x = cursor_x;
    } else if (cursor_x > p_scene->web_url_scroll_x + visible_width) {
        p_scene->web_url_scroll_x = cursor_x - visible_width;
    }
    if (p_scene->web_url_scroll_x < 0.0f) p_scene->web_url_scroll_x = 0.0f;
}

static void Canvas_Web_URL_Copy_Selection(Canvas_Scene *p_scene)
{
    int32 start = 0;
    int32 end = 0;
    Canvas_Web_URL_Selection_Bounds(p_scene, &start, &end);
    if (start == end) return;
    char selected[CANVAS_ENTITY_TEXT_CAPACITY];
    memcpy(
        selected,
        p_scene->web_url_draft + start,
        (size_t)(end - start));
    selected[end - start] = '\0';
    SetClipboardText(selected);
}

static void Canvas_Web_URL_Normalize(char *p_url, size_t capacity)
{
    if (!p_url[0] ||
        strstr(p_url, "://") ||
        strncmp(p_url, "about:", 6) == 0 ||
        strncmp(p_url, "data:", 5) == 0 ||
        strncmp(p_url, "file:", 5) == 0) {
        return;
    }
    char normalized[CANVAS_ENTITY_TEXT_CAPACITY];
    snprintf(normalized, sizeof(normalized), "https://%s", p_url);
    snprintf(p_url, capacity, "%s", normalized);
}

Canvas_Web_Chrome_Action Canvas_Web_Chrome_Update_Input(
    const Canvas_Camera *p_camera,
    Canvas_Scene *p_scene,
    Font font,
    const char *p_current_url)
{
    if (p_scene->selected_index < 0 ||
        p_scene->selected_index >=
            (int32)Dowa_Array_Length(p_scene->p_entities)) {
        p_scene->editing_web_url = FALSE;
        return CANVAS_WEB_CHROME_NONE;
    }
    Canvas_Entity *p_entity =
        &p_scene->p_entities[p_scene->selected_index];
    if (p_entity->type != CANVAS_ENTITY_WEB_CONTENT ||
        p_entity->removing) {
        p_scene->editing_web_url = FALSE;
        return CANVAS_WEB_CHROME_NONE;
    }

    Rectangle toolbar = Canvas_Web_Toolbar_Bounds(p_camera, p_entity);
    Rectangle address = Canvas_Web_Address_Bounds(toolbar, p_camera->zoom);
    Vector2 pointer = Canvas_Camera_Screen_To_World(
        p_camera,
        GetMousePosition());
    if (p_scene->editing_web_url &&
        p_scene->web_url_entity_id == p_entity->id) {
        boolean shortcut = Canvas_Control_Is_Down();
        boolean extend = Canvas_Shift_Is_Down();
        int32 length = (int32)strlen(p_scene->web_url_draft);
        if (shortcut && IsKeyPressed(KEY_A)) {
            p_scene->web_url_selection_anchor = 0;
            p_scene->web_url_cursor = length;
        }
        if (shortcut && IsKeyPressed(KEY_C)) {
            Canvas_Web_URL_Copy_Selection(p_scene);
        }
        if (shortcut && IsKeyPressed(KEY_X)) {
            Canvas_Web_URL_Copy_Selection(p_scene);
            Canvas_Web_URL_Delete_Selection(p_scene);
        }
        if (shortcut && IsKeyPressed(KEY_V)) {
            const char *p_clipboard = GetClipboardText();
            if (p_clipboard) Canvas_Web_URL_Insert(p_scene, p_clipboard);
        }
        if (Canvas_Key_Activated(KEY_LEFT)) {
            int32 cursor = p_scene->web_url_cursor;
            if (!extend &&
                cursor != p_scene->web_url_selection_anchor) {
                int32 end = 0;
                Canvas_Web_URL_Selection_Bounds(p_scene, &cursor, &end);
            } else {
                cursor--;
            }
            if (cursor < 0) cursor = 0;
            p_scene->web_url_cursor = cursor;
            if (!extend) p_scene->web_url_selection_anchor = cursor;
        }
        if (Canvas_Key_Activated(KEY_RIGHT)) {
            int32 cursor = p_scene->web_url_cursor;
            if (!extend &&
                cursor != p_scene->web_url_selection_anchor) {
                int32 start = 0;
                Canvas_Web_URL_Selection_Bounds(p_scene, &start, &cursor);
            } else {
                cursor++;
            }
            if (cursor > length) cursor = length;
            p_scene->web_url_cursor = cursor;
            if (!extend) p_scene->web_url_selection_anchor = cursor;
        }
        if (Canvas_Key_Activated(KEY_HOME)) {
            p_scene->web_url_cursor = 0;
            if (!extend) p_scene->web_url_selection_anchor = 0;
        }
        if (Canvas_Key_Activated(KEY_END)) {
            p_scene->web_url_cursor = length;
            if (!extend) p_scene->web_url_selection_anchor = length;
        }
        if (Canvas_Key_Activated(KEY_BACKSPACE)) {
            if (p_scene->web_url_cursor !=
                p_scene->web_url_selection_anchor) {
                Canvas_Web_URL_Delete_Selection(p_scene);
            } else if (p_scene->web_url_cursor > 0) {
                p_scene->web_url_selection_anchor =
                    p_scene->web_url_cursor - 1;
                Canvas_Web_URL_Delete_Selection(p_scene);
            }
        }
        if (Canvas_Key_Activated(KEY_DELETE)) {
            if (p_scene->web_url_cursor !=
                p_scene->web_url_selection_anchor) {
                Canvas_Web_URL_Delete_Selection(p_scene);
            } else if (p_scene->web_url_cursor < length) {
                p_scene->web_url_selection_anchor =
                    p_scene->web_url_cursor + 1;
                Canvas_Web_URL_Delete_Selection(p_scene);
            }
        }
        int32 codepoint = GetCharPressed();
        while (codepoint > 0) {
            if (!shortcut && codepoint >= 32 && codepoint <= 126) {
                char inserted[2] = {(char)codepoint, '\0'};
                Canvas_Web_URL_Insert(p_scene, inserted);
            }
            codepoint = GetCharPressed();
        }
        Canvas_Web_URL_Ensure_Cursor_Visible(
            p_scene,
            font,
            address,
            p_camera->zoom);
        if (IsKeyPressed(KEY_ESCAPE)) {
            p_scene->editing_web_url = FALSE;
        }
        if (IsKeyPressed(KEY_ENTER)) {
            Canvas_Web_URL_Normalize(
                p_scene->web_url_draft,
                sizeof(p_scene->web_url_draft));
            snprintf(
                p_entity->text,
                sizeof(p_entity->text),
                "%s",
                p_scene->web_url_draft);
            p_scene->editing_web_url = FALSE;
            return CANVAS_WEB_CHROME_NAVIGATE;
        }
    }

    if (!CheckCollisionPointRec(pointer, toolbar)) {
        if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
            p_scene->editing_web_url = FALSE;
            return CANVAS_WEB_CHROME_NONE;
        }
        return p_scene->editing_web_url ?
            CANVAS_WEB_CHROME_CAPTURE :
            CANVAS_WEB_CHROME_NONE;
    }
    if (!IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
        return CANVAS_WEB_CHROME_CAPTURE;
    }
    if (CheckCollisionPointRec(
            pointer,
            Canvas_Web_Toolbar_Button_Bounds(toolbar, p_camera->zoom, 0))) {
        return CANVAS_WEB_CHROME_BACK;
    }
    if (CheckCollisionPointRec(
            pointer,
            Canvas_Web_Toolbar_Button_Bounds(toolbar, p_camera->zoom, 1))) {
        return CANVAS_WEB_CHROME_FORWARD;
    }
    if (CheckCollisionPointRec(pointer, address)) {
        if (!p_scene->editing_web_url ||
            p_scene->web_url_entity_id != p_entity->id) {
            snprintf(
                p_scene->web_url_draft,
                sizeof(p_scene->web_url_draft),
                "%s",
                p_current_url && p_current_url[0] ?
                    p_current_url :
                    p_entity->text);
            p_scene->web_url_entity_id = p_entity->id;
            p_scene->web_url_cursor =
                (int32)strlen(p_scene->web_url_draft);
            p_scene->web_url_selection_anchor = 0;
            p_scene->web_url_scroll_x = 0.0f;
            p_scene->editing_web_url = TRUE;
        } else {
            int32 cursor = Canvas_Web_URL_Cursor_At_Point(
                font,
                p_scene,
                address,
                p_camera->zoom,
                pointer);
            p_scene->web_url_cursor = cursor;
            if (!Canvas_Shift_Is_Down()) {
                p_scene->web_url_selection_anchor = cursor;
            }
        }
        Canvas_Web_URL_Ensure_Cursor_Visible(
            p_scene,
            font,
            address,
            p_camera->zoom);
        return CANVAS_WEB_CHROME_CAPTURE;
    }
    p_scene->editing_web_url = FALSE;
    return CANVAS_WEB_CHROME_CAPTURE;
}

boolean Canvas_Web_Chrome_Is_Editing(const Canvas_Scene *p_scene)
{
    return p_scene->editing_web_url;
}

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,
    };
}

void Canvas_Camera_Fit_Bounds(
    Canvas_Camera *p_camera,
    Rectangle bounds,
    float padding_left,
    float padding_top,
    float padding_right,
    float padding_bottom)
{
    float available_width = fmaxf(
        1.0f,
        (float)p_camera->viewport_width - padding_left - padding_right);
    float available_height = fmaxf(
        1.0f,
        (float)p_camera->viewport_height - padding_top - padding_bottom);
    float zoom = fminf(
        available_width / fmaxf(bounds.width, 1.0f),
        available_height / fmaxf(bounds.height, 1.0f));
    p_camera->zoom = Canvas_Clamp(
        zoom,
        p_camera->min_zoom,
        p_camera->max_zoom);

    Vector2 world_center = {
        bounds.x + bounds.width * 0.5f,
        bounds.y + bounds.height * 0.5f,
    };
    Vector2 desired_screen_center = {
        padding_left + available_width * 0.5f,
        padding_top + available_height * 0.5f,
    };
    Vector2 viewport_center = {
        (float)p_camera->viewport_width * 0.5f,
        (float)p_camera->viewport_height * 0.5f,
    };
    p_camera->target = (Vector2){
        world_center.x -
            (desired_screen_center.x - viewport_center.x) / p_camera->zoom,
        world_center.y -
            (desired_screen_center.y - viewport_center.y) / p_camera->zoom,
    };
}

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 cursor=%d selection_anchor=%d",
                p_entity->active ? "true" : "false",
                p_entity->text_cursor,
                p_entity->text_selection_anchor);
            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;
        case CANVAS_ENTITY_LUCIDE_GALLERY:
            Canvas_Context_Append(
                p_writer,
                "icons=%d query=",
                Canvas_Lucide_Count_Matches(p_entity->text));
            Canvas_Context_Append_Quoted(p_writer, p_entity->text);
            Canvas_Context_Append(
                p_writer,
                " search_focused=%s scroll_offset=%d renderer=\"raylib\"",
                p_entity->active ? "true" : "false",
                p_entity->value);
            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",
            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(
            &writer,
            " visibility=%.2f removing=%s\n"
            "  state: ",
            p_entity->visibility_amount,
            p_entity->removing ? "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,
        .visibility_amount = 0.0f,
        .active = FALSE,
        .pinned = FALSE,
        .removing = 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");
    }
    if (type == CANVAS_ENTITY_LUCIDE_GALLERY) {
        entity.size = (Vector2){1440.0f, 1040.0f};
    }

    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->selecting_text = FALSE;
    p_scene->editing_web_url = FALSE;
    p_scene->web_url_entity_id = 0;
    p_scene->web_url_draft[0] = '\0';
    p_scene->notification_stack_amount = 0.0f;
}

void Canvas_Scene_Fade_Out_All(Canvas_Scene *p_scene)
{
    for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
        p_scene->p_entities[index].removing = TRUE;
        p_scene->p_entities[index].active = FALSE;
    }
    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->selecting_text = FALSE;
    p_scene->editing_web_url = FALSE;
}

void Canvas_Scene_Fade_Out_Selected(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];
    p_entity->removing = TRUE;
    p_entity->active = FALSE;
    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->selecting_text = FALSE;
    p_scene->editing_web_url = FALSE;
}

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

boolean Canvas_Scene_Show_Lucide_Gallery(
    Canvas_Scene *p_scene,
    Canvas_Camera *p_camera)
{
    int32 gallery_index = -1;
    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_LUCIDE_GALLERY) {
            gallery_index = (int32)index;
            break;
        }
    }

    if (gallery_index < 0) {
        if (!Canvas_Scene_Add(
                p_scene,
                CANVAS_ENTITY_LUCIDE_GALLERY,
                (Vector2){1200.0f, -520.0f})) {
            return FALSE;
        }
        gallery_index = (int32)Dowa_Array_Length(p_scene->p_entities) - 1;
    }

    Canvas_Entity *p_gallery = &p_scene->p_entities[gallery_index];
    p_gallery->removing = FALSE;
    p_scene->selected_index = gallery_index;
    Canvas_Camera_Fit_Bounds(
        p_camera,
        (Rectangle){
            p_gallery->position.x,
            p_gallery->position.y,
            p_gallery->size.x,
            p_gallery->size.y,
        },
        320.0f,
        56.0f,
        56.0f,
        56.0f);
    return TRUE;
}

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_LUCIDE_GALLERY:
            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_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->removing) continue;
        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 (p_scene->p_entities[index].removing) continue;
        if (Canvas_Entity_Contains(&p_scene->p_entities[index], world_pointer, zoom)) return index;
    }
    return -1;
}

int32 Canvas_Scene_Topmost_Visual(
    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->removing) continue;
        if (p_entity->type == CANVAS_ENTITY_WEB_CONTENT) {
            if (CheckCollisionPointRec(
                    world_pointer,
                    (Rectangle){
                        p_entity->position.x,
                        p_entity->position.y,
                        p_entity->size.x,
                        p_entity->size.y,
                    })) {
                return index;
            }
            continue;
        }
        if (Canvas_Entity_Contains(p_entity, 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 boolean Canvas_Shift_Is_Down(void)
{
    return IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT);
}

static boolean Canvas_Alt_Is_Down(void)
{
    return IsKeyDown(KEY_LEFT_ALT) || IsKeyDown(KEY_RIGHT_ALT);
}

static boolean Canvas_Super_Is_Down(void)
{
    return IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_RIGHT_SUPER);
}

static boolean Canvas_Key_Activated(int32 key)
{
    return IsKeyPressed(key) || IsKeyPressedRepeat(key);
}

static float Canvas_Text_Measure_Range(
    Font font,
    const char *p_text,
    int32 start,
    int32 end)
{
    if (end <= start) return 0.0f;
    char buffer[CANVAS_ENTITY_TEXT_CAPACITY];
    int32 length = end - start;
    if (length >= (int32)sizeof(buffer)) length = (int32)sizeof(buffer) - 1;
    memcpy(buffer, p_text + start, (size_t)length);
    buffer[length] = '\0';
    return MeasureTextEx(
        font,
        buffer,
        CANVAS_TEXT_AREA_FONT_SIZE,
        CANVAS_TEXT_AREA_SPACING).x;
}

static int32 Canvas_Text_Build_Lines(
    Font font,
    const char *p_text,
    float max_width,
    Canvas_Text_Line *p_lines,
    int32 line_capacity)
{
    int32 length = (int32)strlen(p_text);
    int32 line_count = 0;
    int32 start = 0;

    while (start <= length && line_count < line_capacity) {
        int32 index = start;
        int32 last_space = -1;
        float width = 0.0f;
        boolean ended_with_newline = FALSE;

        while (index < length) {
            if (p_text[index] == '\n') {
                ended_with_newline = TRUE;
                break;
            }
            float character_width = Canvas_Text_Measure_Range(
                font,
                p_text,
                index,
                index + 1);
            if (width + character_width > max_width && index > start) {
                if (last_space >= start) index = last_space + 1;
                break;
            }
            width += character_width;
            if (p_text[index] == ' ' || p_text[index] == '\t') {
                last_space = index;
            }
            index++;
        }

        int32 end = index;
        int32 next = ended_with_newline ? index + 1 : index;
        p_lines[line_count++] = (Canvas_Text_Line){
            .start = start,
            .end = end,
            .next = next,
            .width = Canvas_Text_Measure_Range(font, p_text, start, end),
        };
        if (index >= length) break;
        start = next;
    }

    return line_count;
}

static Rectangle Canvas_Text_Area_Content_Bounds(
    const Canvas_Entity *p_entity)
{
    return (Rectangle){
        p_entity->position.x + CANVAS_TEXT_AREA_PADDING,
        p_entity->position.y + CANVAS_TEXT_AREA_PADDING,
        p_entity->size.x - CANVAS_TEXT_AREA_PADDING * 2.0f,
        p_entity->size.y - CANVAS_TEXT_AREA_PADDING * 2.0f,
    };
}

static int32 Canvas_Text_Line_For_Cursor(
    const Canvas_Text_Line *p_lines,
    int32 line_count,
    int32 cursor)
{
    for (int32 line = 0; line < line_count; line++) {
        if (cursor <= p_lines[line].end) return line;
        if (line + 1 < line_count && cursor < p_lines[line + 1].start) {
            return line;
        }
    }
    return line_count > 0 ? line_count - 1 : 0;
}

static int32 Canvas_Text_Cursor_At_Point(
    Font font,
    const Canvas_Entity *p_entity,
    Vector2 point)
{
    Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY];
    Rectangle content = Canvas_Text_Area_Content_Bounds(p_entity);
    int32 line_count = Canvas_Text_Build_Lines(
        font,
        p_entity->text,
        content.width,
        lines,
        CANVAS_ENTITY_TEXT_CAPACITY);
    int32 line = (int32)floorf(
        (point.y - content.y + p_entity->text_scroll_y) /
        CANVAS_TEXT_AREA_LINE_HEIGHT);
    if (line < 0) line = 0;
    if (line >= line_count) line = line_count - 1;
    float local_x = point.x - content.x;
    if (local_x <= 0.0f) return lines[line].start;

    for (int32 cursor = lines[line].start; cursor < lines[line].end; cursor++) {
        float left = Canvas_Text_Measure_Range(
            font,
            p_entity->text,
            lines[line].start,
            cursor);
        float right = Canvas_Text_Measure_Range(
            font,
            p_entity->text,
            lines[line].start,
            cursor + 1);
        if (local_x < (left + right) * 0.5f) return cursor;
    }
    return lines[line].end;
}

static void Canvas_Text_Selection_Bounds(
    const Canvas_Entity *p_entity,
    int32 *p_start,
    int32 *p_end)
{
    *p_start = p_entity->text_cursor;
    *p_end = p_entity->text_selection_anchor;
    if (*p_start > *p_end) {
        int32 swap = *p_start;
        *p_start = *p_end;
        *p_end = swap;
    }
}

static boolean Canvas_Text_Has_Selection(const Canvas_Entity *p_entity)
{
    return p_entity->text_cursor != p_entity->text_selection_anchor;
}

static void Canvas_Text_Delete_Selection(Canvas_Entity *p_entity)
{
    int32 start = 0;
    int32 end = 0;
    Canvas_Text_Selection_Bounds(p_entity, &start, &end);
    if (start == end) return;
    size_t length = strlen(p_entity->text);
    memmove(
        p_entity->text + start,
        p_entity->text + end,
        length - (size_t)end + 1);
    p_entity->text_cursor = start;
    p_entity->text_selection_anchor = start;
}

static void Canvas_Text_Insert(
    Canvas_Entity *p_entity,
    const char *p_text)
{
    Canvas_Text_Delete_Selection(p_entity);
    size_t length = strlen(p_entity->text);
    size_t available =
        sizeof(p_entity->text) - length - 1;
    if (available == 0) return;

    char filtered[CANVAS_ENTITY_TEXT_CAPACITY];
    size_t filtered_length = 0;
    for (const char *p_cursor = p_text;
        *p_cursor && filtered_length < available;
        p_cursor++) {
        if (*p_cursor == '\r') continue;
        if (*p_cursor == '\n' || *p_cursor == '\t' ||
            (*p_cursor >= 32 && *p_cursor <= 126)) {
            filtered[filtered_length++] = *p_cursor == '\t' ? ' ' : *p_cursor;
        }
    }
    if (filtered_length == 0) return;

    int32 cursor = p_entity->text_cursor;
    memmove(
        p_entity->text + cursor + filtered_length,
        p_entity->text + cursor,
        length - (size_t)cursor + 1);
    memcpy(p_entity->text + cursor, filtered, filtered_length);
    p_entity->text_cursor += (int32)filtered_length;
    p_entity->text_selection_anchor = p_entity->text_cursor;
}

static int32 Canvas_Text_Previous_Word(const char *p_text, int32 cursor)
{
    while (cursor > 0 && p_text[cursor - 1] == ' ') cursor--;
    while (cursor > 0 &&
        p_text[cursor - 1] != ' ' &&
        p_text[cursor - 1] != '\n') {
        cursor--;
    }
    return cursor;
}

static int32 Canvas_Text_Next_Word(const char *p_text, int32 cursor)
{
    int32 length = (int32)strlen(p_text);
    while (cursor < length &&
        p_text[cursor] != ' ' &&
        p_text[cursor] != '\n') {
        cursor++;
    }
    while (cursor < length && p_text[cursor] == ' ') cursor++;
    return cursor;
}

static void Canvas_Text_Move_Cursor(
    Canvas_Entity *p_entity,
    int32 cursor,
    boolean extend_selection)
{
    int32 length = (int32)strlen(p_entity->text);
    if (cursor < 0) cursor = 0;
    if (cursor > length) cursor = length;
    p_entity->text_cursor = cursor;
    if (!extend_selection) p_entity->text_selection_anchor = cursor;
}

static void Canvas_Text_Ensure_Cursor_Visible(
    Canvas_Entity *p_entity,
    Font font)
{
    Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY];
    Rectangle content = Canvas_Text_Area_Content_Bounds(p_entity);
    int32 line_count = Canvas_Text_Build_Lines(
        font,
        p_entity->text,
        content.width,
        lines,
        CANVAS_ENTITY_TEXT_CAPACITY);
    int32 line = Canvas_Text_Line_For_Cursor(
        lines,
        line_count,
        p_entity->text_cursor);
    float cursor_top = (float)line * CANVAS_TEXT_AREA_LINE_HEIGHT;
    float cursor_bottom = cursor_top + CANVAS_TEXT_AREA_LINE_HEIGHT;
    if (cursor_top < p_entity->text_scroll_y) {
        p_entity->text_scroll_y = cursor_top;
    } else if (cursor_bottom >
        p_entity->text_scroll_y + content.height) {
        p_entity->text_scroll_y = cursor_bottom - content.height;
    }
    float max_scroll = fmaxf(
        0.0f,
        (float)line_count * CANVAS_TEXT_AREA_LINE_HEIGHT - content.height);
    p_entity->text_scroll_y = Canvas_Clamp(
        p_entity->text_scroll_y,
        0.0f,
        max_scroll);
}

static void Canvas_Text_Update_Keyboard(Canvas_Entity *p_entity, Font font)
{
    boolean shortcut = Canvas_Control_Is_Down();
    boolean control = IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL);
    boolean super = Canvas_Super_Is_Down();
    boolean word_modifier = control || Canvas_Alt_Is_Down();
    boolean extend = Canvas_Shift_Is_Down();
    int32 length = (int32)strlen(p_entity->text);
    int32 original_cursor = p_entity->text_cursor;
    int32 original_length = length;

    if (shortcut && IsKeyPressed(KEY_A)) {
        p_entity->text_selection_anchor = 0;
        p_entity->text_cursor = length;
    }
    if (shortcut && IsKeyPressed(KEY_C) && Canvas_Text_Has_Selection(p_entity)) {
        int32 start = 0;
        int32 end = 0;
        char selected[CANVAS_ENTITY_TEXT_CAPACITY];
        Canvas_Text_Selection_Bounds(p_entity, &start, &end);
        memcpy(selected, p_entity->text + start, (size_t)(end - start));
        selected[end - start] = '\0';
        SetClipboardText(selected);
    }
    if (shortcut && IsKeyPressed(KEY_X) && Canvas_Text_Has_Selection(p_entity)) {
        int32 start = 0;
        int32 end = 0;
        char selected[CANVAS_ENTITY_TEXT_CAPACITY];
        Canvas_Text_Selection_Bounds(p_entity, &start, &end);
        memcpy(selected, p_entity->text + start, (size_t)(end - start));
        selected[end - start] = '\0';
        SetClipboardText(selected);
        Canvas_Text_Delete_Selection(p_entity);
    }
    if (shortcut && IsKeyPressed(KEY_V)) {
        const char *p_clipboard = GetClipboardText();
        if (p_clipboard) Canvas_Text_Insert(p_entity, p_clipboard);
    }

    if (Canvas_Key_Activated(KEY_LEFT)) {
        int32 cursor = p_entity->text_cursor;
        if (!extend && Canvas_Text_Has_Selection(p_entity)) {
            int32 end = 0;
            Canvas_Text_Selection_Bounds(p_entity, &cursor, &end);
        } else if (super) {
            Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY];
            Rectangle content = Canvas_Text_Area_Content_Bounds(p_entity);
            int32 count = Canvas_Text_Build_Lines(
                font,
                p_entity->text,
                content.width,
                lines,
                CANVAS_ENTITY_TEXT_CAPACITY);
            cursor = lines[Canvas_Text_Line_For_Cursor(
                lines,
                count,
                cursor)].start;
        } else {
            cursor = word_modifier ?
                Canvas_Text_Previous_Word(p_entity->text, cursor) :
                cursor - 1;
        }
        Canvas_Text_Move_Cursor(p_entity, cursor, extend);
    }
    if (Canvas_Key_Activated(KEY_RIGHT)) {
        int32 cursor = p_entity->text_cursor;
        if (!extend && Canvas_Text_Has_Selection(p_entity)) {
            int32 start = 0;
            Canvas_Text_Selection_Bounds(p_entity, &start, &cursor);
        } else if (super) {
            Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY];
            Rectangle content = Canvas_Text_Area_Content_Bounds(p_entity);
            int32 count = Canvas_Text_Build_Lines(
                font,
                p_entity->text,
                content.width,
                lines,
                CANVAS_ENTITY_TEXT_CAPACITY);
            cursor = lines[Canvas_Text_Line_For_Cursor(
                lines,
                count,
                cursor)].end;
        } else {
            cursor = word_modifier ?
                Canvas_Text_Next_Word(p_entity->text, cursor) :
                cursor + 1;
        }
        Canvas_Text_Move_Cursor(p_entity, cursor, extend);
    }

    Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY];
    Rectangle content = Canvas_Text_Area_Content_Bounds(p_entity);
    int32 line_count = Canvas_Text_Build_Lines(
        font,
        p_entity->text,
        content.width,
        lines,
        CANVAS_ENTITY_TEXT_CAPACITY);
    int32 current_line = Canvas_Text_Line_For_Cursor(
        lines,
        line_count,
        p_entity->text_cursor);
    float cursor_x = Canvas_Text_Measure_Range(
        font,
        p_entity->text,
        lines[current_line].start,
        p_entity->text_cursor);
    if (Canvas_Key_Activated(KEY_UP) || Canvas_Key_Activated(KEY_DOWN)) {
        int32 target_line = current_line +
            (IsKeyDown(KEY_UP) ? -1 : 1);
        if (target_line < 0) target_line = 0;
        if (target_line >= line_count) target_line = line_count - 1;
        Vector2 target = {
            content.x + cursor_x,
            content.y +
                (float)target_line * CANVAS_TEXT_AREA_LINE_HEIGHT -
                p_entity->text_scroll_y +
                CANVAS_TEXT_AREA_LINE_HEIGHT * 0.5f,
        };
        Canvas_Text_Move_Cursor(
            p_entity,
            Canvas_Text_Cursor_At_Point(font, p_entity, target),
            extend);
    }
    if (Canvas_Key_Activated(KEY_HOME)) {
        Canvas_Text_Move_Cursor(
            p_entity,
            lines[current_line].start,
            extend);
    }
    if (Canvas_Key_Activated(KEY_END)) {
        Canvas_Text_Move_Cursor(
            p_entity,
            lines[current_line].end,
            extend);
    }

    if (Canvas_Key_Activated(KEY_BACKSPACE)) {
        if (Canvas_Text_Has_Selection(p_entity)) {
            Canvas_Text_Delete_Selection(p_entity);
        } else if (p_entity->text_cursor > 0) {
            int32 start = p_entity->text_cursor - 1;
            if (super) {
                start = lines[Canvas_Text_Line_For_Cursor(
                    lines,
                    line_count,
                    p_entity->text_cursor)].start;
            } else if (word_modifier) {
                start = Canvas_Text_Previous_Word(
                    p_entity->text,
                    p_entity->text_cursor);
            }
            p_entity->text_selection_anchor = start;
            Canvas_Text_Delete_Selection(p_entity);
        }
    }
    if (Canvas_Key_Activated(KEY_DELETE)) {
        if (Canvas_Text_Has_Selection(p_entity)) {
            Canvas_Text_Delete_Selection(p_entity);
        } else if (p_entity->text_cursor < length) {
            int32 end = p_entity->text_cursor + 1;
            if (super) {
                end = lines[Canvas_Text_Line_For_Cursor(
                    lines,
                    line_count,
                    p_entity->text_cursor)].end;
            } else if (word_modifier) {
                end = Canvas_Text_Next_Word(
                    p_entity->text,
                    p_entity->text_cursor);
            }
            p_entity->text_selection_anchor = end;
            Canvas_Text_Delete_Selection(p_entity);
        }
    }
    if (IsKeyPressed(KEY_ENTER)) Canvas_Text_Insert(p_entity, "\n");

    int32 codepoint = GetCharPressed();
    while (codepoint > 0) {
        if (!shortcut && codepoint >= 32 && codepoint <= 126) {
            char inserted[2] = {(char)codepoint, '\0'};
            Canvas_Text_Insert(p_entity, inserted);
        }
        codepoint = GetCharPressed();
    }
    if (p_entity->text_cursor != original_cursor ||
        (int32)strlen(p_entity->text) != original_length) {
        Canvas_Text_Ensure_Cursor_Visible(p_entity, font);
    }
}

static void Canvas_Scene_Update_Text_Input(
    Canvas_Scene *p_scene,
    Font font)
{
    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->type != CANVAS_ENTITY_LUCIDE_GALLERY) ||
        !p_entity->active) {
        return;
    }

    if (p_entity->type == CANVAS_ENTITY_TEXT_AREA) {
        Canvas_Text_Update_Keyboard(p_entity, font);
        return;
    }

    size_t length = strlen(p_entity->text);
    boolean changed = FALSE;
    int32 codepoint = GetCharPressed();
    while (codepoint > 0) {
        size_t input_capacity = p_entity->type == CANVAS_ENTITY_LUCIDE_GALLERY ?
            64 :
            sizeof(p_entity->text);
        if (codepoint >= 32 &&
            codepoint <= 126 &&
            length + 1 < input_capacity) {
            p_entity->text[length++] = p_entity->type == CANVAS_ENTITY_LUCIDE_GALLERY ?
                Canvas_Ascii_Lower((char)codepoint) :
                (char)codepoint;
            p_entity->text[length] = '\0';
            changed = TRUE;
        }
        codepoint = GetCharPressed();
    }

    if (p_entity->type == CANVAS_ENTITY_TEXT_AREA &&
        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';
        changed = TRUE;
    }
    if (changed && p_entity->type == CANVAS_ENTITY_LUCIDE_GALLERY) {
        p_entity->value = 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_Adjust_Index_After_Removal(
    int32 *p_index,
    int32 removed_index)
{
    if (*p_index == removed_index) {
        *p_index = -1;
    } else if (*p_index > removed_index) {
        (*p_index)--;
    }
}

static void Canvas_Scene_Update_Lifecycle(Canvas_Scene *p_scene)
{
    float follow = 1.0f - expf(-GetFrameTime() * 9.0f);
    size_t index = 0;
    while (index < Dowa_Array_Length(p_scene->p_entities)) {
        Canvas_Entity *p_entity = &p_scene->p_entities[index];
        float target = p_entity->removing ? 0.0f : 1.0f;
        p_entity->visibility_amount +=
            (target - p_entity->visibility_amount) * follow;
        p_entity->visibility_amount = Canvas_Clamp(
            p_entity->visibility_amount,
            0.0f,
            1.0f);
        float chrome_target =
            p_entity->type == CANVAS_ENTITY_WEB_CONTENT &&
            (int32)index == p_scene->selected_index &&
            !p_entity->removing ?
                1.0f :
                0.0f;
        float chrome_follow = 1.0f - expf(-GetFrameTime() * 12.0f);
        p_entity->web_chrome_amount +=
            (chrome_target - p_entity->web_chrome_amount) * chrome_follow;
        p_entity->web_chrome_amount = Canvas_Clamp(
            p_entity->web_chrome_amount,
            0.0f,
            1.0f);

        if (p_entity->removing && p_entity->visibility_amount <= 0.01f) {
            int32 removed_index = (int32)index;
            size_t length = Dowa_Array_Length(p_scene->p_entities);
            for (size_t move = index + 1; move < length; move++) {
                p_scene->p_entities[move - 1] = p_scene->p_entities[move];
            }
            (void)Dowa_Array_Pop(p_scene->p_entities);
            Canvas_Scene_Adjust_Index_After_Removal(
                &p_scene->selected_index,
                removed_index);
            Canvas_Scene_Adjust_Index_After_Removal(
                &p_scene->pressed_index,
                removed_index);
            Canvas_Scene_Adjust_Index_After_Removal(
                &p_scene->hover_index,
                removed_index);
            continue;
        }
        index++;
    }
}

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,
    Font font,
    boolean block_pointer_input)
{
    Canvas_Scene_Update_Lifecycle(p_scene);
    Canvas_Scene_Update_Text_Input(p_scene, font);
    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];
        boolean control_down = Canvas_Control_Is_Down();
        if (p_hovered->type == CANVAS_ENTITY_TEXT_AREA &&
            p_hovered->active &&
            !control_down) {
            float wheel = GetMouseWheelMove();
            if (wheel != 0.0f) {
                Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY];
                Rectangle content = Canvas_Text_Area_Content_Bounds(p_hovered);
                int32 line_count = Canvas_Text_Build_Lines(
                    font,
                    p_hovered->text,
                    content.width,
                    lines,
                    CANVAS_ENTITY_TEXT_CAPACITY);
                float max_scroll = fmaxf(
                    0.0f,
                    (float)line_count * CANVAS_TEXT_AREA_LINE_HEIGHT -
                        content.height);
                p_hovered->text_scroll_y = Canvas_Clamp(
                    p_hovered->text_scroll_y -
                        wheel * CANVAS_TEXT_AREA_LINE_HEIGHT * 2.0f,
                    0.0f,
                    max_scroll);
                consumed_scroll = TRUE;
            }
        } else if (p_hovered->type == CANVAS_ENTITY_SCROLL_AREA &&
            !control_down) {
            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;
            }
        } else if (p_hovered->type == CANVAS_ENTITY_LUCIDE_GALLERY &&
            !control_down) {
            float wheel = GetMouseWheelMove();
            if (wheel != 0.0f) {
                p_hovered->value = (int32)Canvas_Clamp(
                    (float)p_hovered->value - wheel * 72.0f,
                    0.0f,
                    Canvas_Lucide_Max_Scroll(p_hovered));
                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 ((int32)index != p_scene->selected_index &&
                p_scene->p_entities[index].type == CANVAS_ENTITY_LUCIDE_GALLERY) {
                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];
            if (p_entity->type == CANVAS_ENTITY_LUCIDE_GALLERY) {
                p_entity->active = CheckCollisionPointRec(
                    world_pointer,
                    Canvas_Lucide_Search_Bounds(p_entity)) ? TRUE : FALSE;
            }
            p_scene->selecting_text = FALSE;
            if (p_entity->type == CANVAS_ENTITY_TEXT_AREA) {
                boolean inside_content = CheckCollisionPointRec(
                    world_pointer,
                    Canvas_Text_Area_Content_Bounds(p_entity)) ? TRUE : FALSE;
                p_entity->active = inside_content;
                if (inside_content) {
                    int32 cursor = Canvas_Text_Cursor_At_Point(
                        font,
                        p_entity,
                        world_pointer);
                    p_entity->text_cursor = cursor;
                    if (!Canvas_Shift_Is_Down()) {
                        p_entity->text_selection_anchor = cursor;
                    }
                    p_scene->selecting_text = TRUE;
                }
            }
            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);
        Canvas_Entity *p_pressed =
            &p_scene->p_entities[p_scene->pressed_index];
        if (p_scene->selecting_text &&
            p_pressed->type == CANVAS_ENTITY_TEXT_AREA) {
            p_pressed->text_cursor = Canvas_Text_Cursor_At_Point(
                font,
                p_pressed,
                world_pointer);
            Canvas_Text_Ensure_Cursor_Visible(p_pressed, font);
        }
        if (screen_distance_squared >= 16.0f &&
            !p_scene->selecting_text &&
            !(p_pressed->type == CANVAS_ENTITY_LUCIDE_GALLERY &&
                p_pressed->active)) {
            p_scene->dragging = TRUE;
        }

        if (p_scene->dragging) {
            Canvas_Entity *p_entity = p_pressed;
            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_Entity *p_entity = &p_scene->p_entities[p_scene->pressed_index];
            if (p_entity->type != CANVAS_ENTITY_TEXT_AREA) {
                Canvas_Scene_Activate(
                    p_scene,
                    p_scene->pressed_index,
                    world_pointer);
            }
            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;
        p_scene->selecting_text = 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->type == CANVAS_ENTITY_LUCIDE_GALLERY) &&
        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) {
            if (Canvas_Control_Is_Down()) {
                Canvas_Camera_Zoom_At(
                    p_camera,
                    GetMousePosition(),
                    powf(1.15f, wheel));
            } else {
                Canvas_Camera_Pan(
                    p_camera,
                    (Vector2){0.0f, wheel * 72.0f});
            }
        }
    }

    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 = Canvas_Color_Fade(p_theme->border, 0.36f);
    Color major = Canvas_Color_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,
    Color selection)
{
    float line = 2.0f / zoom;
    float padding = 7.0f / zoom;

    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,
                Canvas_Color_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_LUCIDE_GALLERY: {
            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);
            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,
                },
                Canvas_Color_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_Text_Area_Content(
    const Canvas_Entity *p_entity,
    Font font,
    const Canvas_Theme *p_theme)
{
    Rectangle content = Canvas_Text_Area_Content_Bounds(p_entity);
    if (!p_entity->text[0]) {
        DrawTextEx(
            font,
            "Write something...",
            (Vector2){content.x, content.y},
            CANVAS_TEXT_AREA_FONT_SIZE,
            CANVAS_TEXT_AREA_SPACING,
            p_theme->text_muted);
        if (p_entity->active && fmod(GetTime(), 1.0) < 0.55) {
            DrawLineEx(
                (Vector2){content.x, content.y},
                (Vector2){
                    content.x,
                    content.y + CANVAS_TEXT_AREA_FONT_SIZE + 2.0f,
                },
                1.5f,
                p_theme->accent);
        }
        return;
    }

    Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY];
    int32 line_count = Canvas_Text_Build_Lines(
        font,
        p_entity->text,
        content.width,
        lines,
        CANVAS_ENTITY_TEXT_CAPACITY);
    int32 selection_start = 0;
    int32 selection_end = 0;
    Canvas_Text_Selection_Bounds(
        p_entity,
        &selection_start,
        &selection_end);
    int32 cursor_line = Canvas_Text_Line_For_Cursor(
        lines,
        line_count,
        p_entity->text_cursor);

    for (int32 line = 0; line < line_count; line++) {
        float y = content.y +
            (float)line * CANVAS_TEXT_AREA_LINE_HEIGHT -
            p_entity->text_scroll_y;
        if (y < content.y ||
            y + CANVAS_TEXT_AREA_FONT_SIZE > content.y + content.height) {
            continue;
        }

        int32 line_selection_start =
            selection_start > lines[line].start ?
                selection_start :
                lines[line].start;
        int32 line_selection_end =
            selection_end < lines[line].end ?
                selection_end :
                lines[line].end;
        if (line_selection_end > line_selection_start) {
            float selection_x = content.x + Canvas_Text_Measure_Range(
                font,
                p_entity->text,
                lines[line].start,
                line_selection_start);
            float selection_width = Canvas_Text_Measure_Range(
                font,
                p_entity->text,
                line_selection_start,
                line_selection_end);
            DrawRectangleRec(
                (Rectangle){
                    selection_x,
                    y - 1.0f,
                    fmaxf(selection_width, 2.0f),
                    CANVAS_TEXT_AREA_LINE_HEIGHT,
                },
                p_theme->accent_soft);
        }

        char line_text[CANVAS_ENTITY_TEXT_CAPACITY];
        int32 line_length = lines[line].end - lines[line].start;
        memcpy(
            line_text,
            p_entity->text + lines[line].start,
            (size_t)line_length);
        line_text[line_length] = '\0';
        DrawTextEx(
            font,
            line_text,
            (Vector2){content.x, y},
            CANVAS_TEXT_AREA_FONT_SIZE,
            CANVAS_TEXT_AREA_SPACING,
            p_theme->text);

        if (p_entity->active &&
            line == cursor_line &&
            fmod(GetTime(), 1.0) < 0.55) {
            float cursor_x = content.x + Canvas_Text_Measure_Range(
                font,
                p_entity->text,
                lines[line].start,
                p_entity->text_cursor);
            DrawLineEx(
                (Vector2){cursor_x, y},
                (Vector2){
                    cursor_x,
                    y + CANVAS_TEXT_AREA_FONT_SIZE + 2.0f,
                },
                1.5f,
                p_theme->accent);
        }
    }
}

static void Canvas_Draw_Entity(
    const Canvas_Entity *p_entity,
    Font font,
    float zoom,
    boolean selected,
    boolean pressed,
    const Canvas_Theme *p_theme)
{
    float opacity = Canvas_Entity_Visibility(p_entity);
    if (opacity <= 0.001f) return;
    Canvas_Entity faded_entity = *p_entity;
    faded_entity.color = Canvas_Color_With_Opacity(
        faded_entity.color,
        opacity);
    Canvas_Theme faded_theme = Canvas_Theme_With_Opacity(p_theme, opacity);
    p_entity = &faded_entity;
    p_theme = &faded_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,
                Canvas_Color_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,
                p_theme->entity_outline);
            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 ?
                p_theme->button_active :
                p_theme->button;
            Color hover_fill = p_entity->active ?
                p_theme->button_active_hover :
                p_theme->button_hover;
            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,
                p_theme->on_accent);
            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);
            Canvas_Draw_Text_Area_Content(p_entity, font, p_theme);
            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,
                    Canvas_Color_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,
                        Canvas_Color_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 :
                    Canvas_Color_Fade(p_entity->color, 0.22f));
            DrawCircleV(
                (Vector2){bounds.x + 44.0f, bounds.y + 58.0f},
                18.0f,
                p_entity->active ?
                    p_theme->accent :
                    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 ?
                        p_theme->on_accent :
                        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 off_track = Canvas_Color_Lerp(
                p_theme->switch_track_off,
                p_theme->switch_track_off_hover,
                p_entity->hover_amount);
            Color on_track = Canvas_Color_Lerp(
                p_theme->switch_track_on,
                p_theme->switch_track_on_hover,
                p_entity->hover_amount);
            Color track = Canvas_Color_Lerp(off_track, on_track, eased);
            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,
                p_theme->switch_thumb);
            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,
                Canvas_Color_Fade(p_theme->toast_surface, opacity));
            DrawRectangleRoundedLinesEx(
                bounds,
                0.20f,
                16,
                1.0f / zoom,
                Canvas_Color_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,
                Canvas_Color_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,
                Canvas_Color_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,
                Canvas_Color_Fade(p_theme->surface_muted, opacity));
            DrawRectangleRoundedLinesEx(
                undo,
                0.26f,
                10,
                1.0f / zoom,
                Canvas_Color_Fade(p_theme->border, opacity));
            DrawTextEx(
                font,
                "Undo",
                (Vector2){undo.x + 12.0f * unit, undo.y + 10.0f * unit},
                13.0f * unit,
                0.0f,
                Canvas_Color_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,
                Canvas_Color_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,
                Canvas_Color_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,
                Canvas_Color_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;
        }
        case CANVAS_ENTITY_LUCIDE_GALLERY: {
            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);
            DrawRectangleRoundedLinesEx(
                bounds,
                0.04f,
                14,
                1.0f / zoom,
                p_theme->border);
            DrawTextEx(
                font,
                "Lucide icons",
                (Vector2){bounds.x + 24.0f, bounds.y + 22.0f},
                26.0f,
                0.0f,
                p_theme->text);
            DrawTextEx(
                font,
                TextFormat(
                    "%d of %d icons - Raylib vectors",
                    Canvas_Lucide_Count_Matches(p_entity->text),
                    CANVAS_LUCIDE_ICON_COUNT),
                (Vector2){bounds.x + 24.0f, bounds.y + 56.0f},
                13.0f,
                0.0f,
                p_theme->text_muted);
            Rectangle search = Canvas_Lucide_Search_Bounds(p_entity);
            DrawRectangleRounded(
                search,
                0.24f,
                10,
                p_entity->active ? p_theme->surface : p_theme->surface_muted);
            DrawRectangleRoundedLinesEx(
                search,
                0.24f,
                10,
                (p_entity->active ? 2.0f : 1.0f) / zoom,
                p_entity->active ? p_theme->accent : p_theme->border);
            const char *p_search_text =
                p_entity->text[0] ? p_entity->text : "Search icons";
            DrawTextEx(
                font,
                p_search_text,
                (Vector2){search.x + 16.0f, search.y + 13.0f},
                14.0f,
                0.0f,
                p_entity->text[0] ? p_theme->text : p_theme->text_muted);
            if (p_entity->active) {
                Vector2 query_size = MeasureTextEx(
                    font,
                    p_entity->text,
                    14.0f,
                    0.0f);
                float caret_x = search.x + 16.0f + query_size.x + 1.0f;
                DrawLineEx(
                    (Vector2){caret_x, search.y + 11.0f},
                    (Vector2){caret_x, search.y + 31.0f},
                    1.0f / zoom,
                    p_theme->accent);
            }
            DrawLineEx(
                (Vector2){bounds.x, bounds.y + CANVAS_LUCIDE_HEADER_HEIGHT},
                (Vector2){
                    bounds.x + bounds.width,
                    bounds.y + CANVAS_LUCIDE_HEADER_HEIGHT,
                },
                1.0f / zoom,
                p_theme->border);

            float content_top = bounds.y + CANVAS_LUCIDE_HEADER_HEIGHT;
            float content_bottom = bounds.y + bounds.height - CANVAS_LUCIDE_PADDING;
            int32 visible_index = 0;
            for (int32 icon_index = 0;
                icon_index < CANVAS_LUCIDE_ICON_COUNT;
                icon_index++) {
                const Canvas_Lucide_Icon *p_icon =
                    &CANVAS_LUCIDE_ICONS[icon_index];
                if (!Canvas_Lucide_Name_Matches(
                        p_icon->p_name,
                        p_entity->text)) {
                    continue;
                }
                int32 row = visible_index / CANVAS_LUCIDE_COLUMNS;
                int32 column = visible_index % CANVAS_LUCIDE_COLUMNS;
                visible_index++;
                float card_x =
                    bounds.x + CANVAS_LUCIDE_PADDING +
                    (float)column *
                        (CANVAS_LUCIDE_CARD_WIDTH + CANVAS_LUCIDE_GAP);
                float card_y =
                    content_top + CANVAS_LUCIDE_PADDING +
                    (float)row *
                        (CANVAS_LUCIDE_CARD_HEIGHT + CANVAS_LUCIDE_GAP) -
                    (float)p_entity->value;
                if (card_y < content_top ||
                    card_y + CANVAS_LUCIDE_CARD_HEIGHT > content_bottom) {
                    continue;
                }
                Rectangle card = {
                    card_x,
                    card_y,
                    CANVAS_LUCIDE_CARD_WIDTH,
                    CANVAS_LUCIDE_CARD_HEIGHT,
                };
                DrawRectangleRounded(card, 0.10f, 8, p_theme->surface_muted);
                DrawRectangleRoundedLinesEx(
                    card,
                    0.10f,
                    8,
                    1.0f / zoom,
                    p_theme->border);

                float icon_scale = 2.0f;
                Vector2 icon_origin = {
                    card.x + (card.width - 24.0f * icon_scale) * 0.5f,
                    card.y + 13.0f,
                };
                Canvas_Lucide_Draw_Icon(
                    p_icon,
                    icon_origin,
                    icon_scale,
                    1.8f,
                    p_theme->text);
                Vector2 label_size = MeasureTextEx(
                    font,
                    p_icon->p_name,
                    11.0f,
                    0.0f);
                float label_x = card.x + (card.width - label_size.x) * 0.5f;
                if (label_x < card.x + 5.0f) label_x = card.x + 5.0f;
                DrawTextEx(
                    font,
                    p_icon->p_name,
                    (Vector2){label_x, card.y + 73.0f},
                    11.0f,
                    0.0f,
                    p_theme->text_muted);
            }

            float max_scroll = Canvas_Lucide_Max_Scroll(p_entity);
            if (max_scroll > 0.0f) {
                float track_height = bounds.height - CANVAS_LUCIDE_HEADER_HEIGHT - 24.0f;
                float thumb_height = fmaxf(
                    36.0f,
                    track_height * bounds.height /
                        Canvas_Lucide_Content_Height(p_entity->text));
                float thumb_y =
                    content_top + 12.0f +
                    ((float)p_entity->value / max_scroll) *
                        (track_height - thumb_height);
                DrawRectangleRounded(
                    (Rectangle){
                        bounds.x + bounds.width - 10.0f,
                        thumb_y,
                        5.0f,
                        thumb_height,
                    },
                    0.50f,
                    6,
                    Canvas_Color_Fade(p_theme->text_muted, 0.72f));
            }
            break;
        }
        default:
            break;
    }

    if (p_entity->pinned) {
        const Canvas_Lucide_Icon *p_pin = Canvas_Lucide_Find_Icon("pin");
        float scale = 0.72f / zoom;
        Vector2 origin = {
            p_entity->position.x + p_entity->size.x - 21.0f / zoom,
            p_entity->position.y + 3.0f / zoom,
        };
        Canvas_Lucide_Draw_Icon(
            p_pin,
            origin,
            scale,
            1.8f / zoom,
            p_theme->pin);
    }
    if (selected && p_entity->type != CANVAS_ENTITY_NOTIFICATION) {
        Canvas_Draw_Selection(p_entity, zoom, p_theme->selection);
    }
}

static void Canvas_Draw_Dropdown_Menu(
    const Canvas_Entity *p_entity,
    Font font,
    Vector2 world_pointer,
    float zoom,
    const Canvas_Theme *p_theme)
{
    float opacity = Canvas_Entity_Visibility(p_entity);
    if (opacity <= 0.001f) return;
    Canvas_Theme faded_theme = Canvas_Theme_With_Opacity(p_theme, opacity);
    p_theme = &faded_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);
        }
    }
}

static Camera2D Canvas_Raylib_Camera(const Canvas_Camera *p_camera)
{
    return (Camera2D){
        .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,
    };
}

void Canvas_Draw_World_Background(
    const Canvas_Camera *p_camera,
    const Canvas_Scene *p_scene,
    const Canvas_Theme *p_theme)
{
    BeginMode2D(Canvas_Raylib_Camera(p_camera));
    if (p_scene->show_grid) Canvas_Draw_Grid(p_camera, p_theme);
    EndMode2D();
}

void Canvas_Draw_World_Entity(
    const Canvas_Camera *p_camera,
    const Canvas_Scene *p_scene,
    size_t entity_index,
    Font font,
    const Canvas_Theme *p_theme)
{
    if (entity_index >= Dowa_Array_Length(p_scene->p_entities)) return;
    BeginMode2D(Canvas_Raylib_Camera(p_camera));
    Canvas_Draw_Entity(
        &p_scene->p_entities[entity_index],
        font,
        p_camera->zoom,
        (int32)entity_index == p_scene->selected_index,
        (int32)entity_index == p_scene->pressed_index &&
            IsMouseButtonDown(MOUSE_BUTTON_LEFT),
        p_theme);
    EndMode2D();
}

void Canvas_Draw_Web_Chrome_Entity(
    const Canvas_Camera *p_camera,
    const Canvas_Scene *p_scene,
    size_t entity_index,
    Font font,
    const Canvas_Theme *p_theme,
    const char *p_url,
    boolean can_go_back,
    boolean can_go_forward)
{
    if (entity_index >= Dowa_Array_Length(p_scene->p_entities)) return;
    const Canvas_Entity *p_entity = &p_scene->p_entities[entity_index];
    if (p_entity->type != CANVAS_ENTITY_WEB_CONTENT ||
        p_entity->web_chrome_amount <= 0.01f) {
        return;
    }

    float zoom = p_camera->zoom;
    float opacity = Canvas_Entity_Visibility(p_entity);
    Canvas_Theme faded_theme = Canvas_Theme_With_Opacity(p_theme, opacity);
    p_theme = &faded_theme;
    Rectangle toolbar = Canvas_Web_Toolbar_Bounds(p_camera, p_entity);
    Rectangle back = Canvas_Web_Toolbar_Button_Bounds(toolbar, zoom, 0);
    Rectangle forward = Canvas_Web_Toolbar_Button_Bounds(toolbar, zoom, 1);
    Rectangle address = Canvas_Web_Address_Bounds(toolbar, zoom);
    Vector2 pointer = Canvas_Camera_Screen_To_World(
        p_camera,
        GetMousePosition());

    BeginMode2D(Canvas_Raylib_Camera(p_camera));
    Vector2 toolbar_screen = Canvas_Camera_World_To_Screen(
        p_camera,
        (Vector2){toolbar.x, toolbar.y});
    BeginScissorMode(
        (int32)toolbar_screen.x,
        (int32)toolbar_screen.y,
        (int32)(toolbar.width * zoom),
        (int32)fmaxf(1.0f, toolbar.height * zoom));
    DrawRectangleRounded(toolbar, 0.18f, 10, p_theme->surface);
    DrawRectangleRoundedLinesEx(
        toolbar,
        0.18f,
        10,
        1.0f / zoom,
        p_theme->border);

    Rectangle buttons[] = {back, forward};
    boolean enabled[] = {can_go_back, can_go_forward};
    const char *icon_names[] = {"arrow-left", "arrow-right"};
    for (int32 index = 0; index < 2; index++) {
        boolean hovered =
            CheckCollisionPointRec(pointer, buttons[index]) && enabled[index];
        if (hovered) {
            DrawRectangleRounded(
                buttons[index],
                0.28f,
                8,
                p_theme->accent_soft);
        }
        Canvas_Lucide_Draw_Icon(
            Canvas_Lucide_Find_Icon(icon_names[index]),
            (Vector2){
                buttons[index].x + 5.5f / zoom,
                buttons[index].y + 5.5f / zoom,
            },
            0.71f / zoom,
            1.8f / zoom,
            enabled[index] ? p_theme->text : p_theme->text_muted);
    }

    DrawRectangleRounded(address, 0.32f, 8, p_theme->surface_muted);
    DrawRectangleRoundedLinesEx(
        address,
        0.32f,
        8,
        1.0f / zoom,
        p_theme->border);
    boolean editing =
        p_scene->editing_web_url &&
        p_scene->web_url_entity_id == p_entity->id;
    const char *p_display_url = editing ?
        p_scene->web_url_draft :
        p_url;
    char visible_url[CANVAS_ENTITY_TEXT_CAPACITY] = {0};
    snprintf(
        visible_url,
        sizeof(visible_url),
        "%s",
        p_display_url && p_display_url[0] ? p_display_url : "about:blank");
    float font_size = 13.0f / zoom;
    float max_width = address.width - 18.0f / zoom;
    size_t url_length = strlen(visible_url);
    while (!editing && url_length > 3 &&
        MeasureTextEx(font, visible_url, font_size, 0.0f).x > max_width) {
        url_length--;
        visible_url[url_length] = '\0';
    }
    if (!editing &&
        p_display_url &&
        strlen(p_display_url) > url_length &&
        url_length > 3) {
        visible_url[url_length - 3] = '.';
        visible_url[url_length - 2] = '.';
        visible_url[url_length - 1] = '.';
    }
    Vector2 address_screen = Canvas_Camera_World_To_Screen(
        p_camera,
        (Vector2){address.x, address.y});
    float address_clip_height = fminf(
        address.height,
        toolbar.y + toolbar.height - address.y);
    if (address_clip_height < 1.0f / zoom) {
        EndScissorMode();
        EndMode2D();
        return;
    }
    BeginScissorMode(
        (int32)address_screen.x,
        (int32)address_screen.y,
        (int32)(address.width * zoom),
        (int32)(address_clip_height * zoom));
    float text_x = address.x + 9.0f / zoom -
        (editing ? p_scene->web_url_scroll_x : 0.0f);
    if (editing &&
        p_scene->web_url_cursor != p_scene->web_url_selection_anchor) {
        int32 selection_start = 0;
        int32 selection_end = 0;
        Canvas_Web_URL_Selection_Bounds(
            p_scene,
            &selection_start,
            &selection_end);
        char prefix[CANVAS_ENTITY_TEXT_CAPACITY];
        char selected[CANVAS_ENTITY_TEXT_CAPACITY];
        memcpy(prefix, visible_url, (size_t)selection_start);
        prefix[selection_start] = '\0';
        memcpy(
            selected,
            visible_url + selection_start,
            (size_t)(selection_end - selection_start));
        selected[selection_end - selection_start] = '\0';
        float selection_x =
            text_x + MeasureTextEx(font, prefix, font_size, 0.0f).x;
        float selection_width =
            MeasureTextEx(font, selected, font_size, 0.0f).x;
        DrawRectangleRec(
            (Rectangle){
                selection_x,
                address.y + 5.0f / zoom,
                selection_width,
                18.0f / zoom,
            },
            p_theme->accent_soft);
    }
    DrawTextEx(
        font,
        visible_url,
        (Vector2){
            text_x,
            address.y + 7.0f / zoom,
        },
        font_size,
        0.0f,
        editing ? p_theme->text : p_theme->text_muted);
    if (editing && ((int32)(GetTime() * 2.0) % 2) == 0) {
        char prefix[CANVAS_ENTITY_TEXT_CAPACITY];
        memcpy(
            prefix,
            visible_url,
            (size_t)p_scene->web_url_cursor);
        prefix[p_scene->web_url_cursor] = '\0';
        float caret_x =
            text_x + MeasureTextEx(font, prefix, font_size, 0.0f).x;
        DrawLineEx(
            (Vector2){caret_x, address.y + 6.0f / zoom},
            (Vector2){caret_x, address.y + 22.0f / zoom},
            1.5f / zoom,
            p_theme->accent);
    }
    EndScissorMode();
    EndMode2D();
}

void Canvas_Draw_World_Overlays(
    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());
    BeginMode2D(Canvas_Raylib_Camera(p_camera));
    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();
}

void Canvas_Draw_World(
    const Canvas_Camera *p_camera,
    const Canvas_Scene *p_scene,
    Font font,
    const Canvas_Theme *p_theme)
{
    Canvas_Draw_World_Background(p_camera, p_scene, p_theme);
    for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
        Canvas_Draw_World_Entity(
            p_camera,
            p_scene,
            index,
            font,
            p_theme);
    }
    Canvas_Draw_World_Overlays(p_camera, p_scene, font, p_theme);
}

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";
        case CANVAS_ENTITY_LUCIDE_GALLERY: return "Lucide gallery";
        default: return "Unknown";
    }
}