diff 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 diff
--- a/infinite_canvas/canvas.c	Mon Aug 17 17:01:40 2026 -0700
+++ b/infinite_canvas/canvas.c	Mon Aug 17 22:16:14 2026 -0700
@@ -1,4 +1,5 @@
 #include "infinite_canvas/canvas.h"
+#include "infinite_canvas/generated/lucide_data.h"
 
 #include <math.h>
 #include <stdarg.h>
@@ -25,6 +26,26 @@
     "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;
@@ -55,6 +76,554 @@
     };
 }
 
+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){
@@ -132,6 +701,48 @@
     };
 }
 
+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) {
@@ -256,8 +867,10 @@
             Canvas_Context_Append_Quoted(p_writer, p_entity->text);
             Canvas_Context_Append(
                 p_writer,
-                " editing=%s",
-                p_entity->active ? "true" : "false");
+                " 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=");
@@ -336,6 +949,18 @@
             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;
@@ -377,8 +1002,7 @@
         snapshot.visible_count++;
         Canvas_Context_Append(
             &writer,
-            "- id=%u type=\"%s\" position=[%.1f, %.1f] size=[%.1f, %.1f] pinned=%s\n"
-            "  state: ",
+            "- 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,
@@ -386,6 +1010,12 @@
             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");
     }
@@ -430,8 +1060,10 @@
         .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,
     };
@@ -492,6 +1124,9 @@
         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;
@@ -506,9 +1141,46 @@
     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);
@@ -552,6 +1224,47 @@
     }
 }
 
+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};
@@ -623,6 +1336,15 @@
                     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(
@@ -683,6 +1405,7 @@
 {
     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)) {
@@ -691,11 +1414,43 @@
     }
 
     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;
@@ -763,30 +1518,536 @@
     }
 }
 
-static void Canvas_Scene_Update_Text_Input(Canvas_Scene *p_scene)
+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->active) return;
+    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) {
-        if (codepoint >= 32 && codepoint <= 126 && length + 1 < sizeof(p_entity->text)) {
-            p_entity->text[length++] = (char)codepoint;
+        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 (IsKeyPressed(KEY_ENTER) && length + 1 < sizeof(p_entity->text)) {
+    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;
     }
 }
 
@@ -802,6 +2063,66 @@
     };
 }
 
+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)
@@ -843,9 +2164,11 @@
 boolean Canvas_Scene_Update_Interaction(
     Canvas_Scene *p_scene,
     const Canvas_Camera *p_camera,
+    Font font,
     boolean block_pointer_input)
 {
-    Canvas_Scene_Update_Text_Input(p_scene);
+    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) {
@@ -859,7 +2182,33 @@
     boolean consumed_scroll = FALSE;
     if (p_scene->hover_index >= 0) {
         Canvas_Entity *p_hovered = &p_scene->p_entities[p_scene->hover_index];
-        if (p_hovered->type == CANVAS_ENTITY_SCROLL_AREA) {
+        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(
@@ -868,6 +2217,16 @@
                     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;
+            }
         }
     }
 
@@ -889,10 +2248,37 @@
                 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(
@@ -913,10 +2299,25 @@
         float screen_distance_squared =
             (delta.x * p_camera->zoom) * (delta.x * p_camera->zoom) +
             (delta.y * p_camera->zoom) * (delta.y * p_camera->zoom);
-        if (screen_distance_squared >= 16.0f) p_scene->dragging = TRUE;
+        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_scene->p_entities[p_scene->pressed_index];
+            Canvas_Entity *p_entity = p_pressed;
             if (p_entity->type == CANVAS_ENTITY_NOTIFICATION) {
                 p_scene->dragging = FALSE;
             } else if (p_scene->resizing) {
@@ -936,13 +2337,19 @@
 
     if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) {
         if (p_scene->pressed_index >= 0 && !p_scene->dragging) {
-            Canvas_Scene_Activate(p_scene, p_scene->pressed_index, world_pointer);
             Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->pressed_index];
+            if (p_entity->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;
 }
@@ -955,7 +2362,9 @@
     }
 
     const Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index];
-    return (p_entity->type == CANVAS_ENTITY_TEXT_AREA && p_entity->active) ? TRUE : FALSE;
+    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(
@@ -1161,10 +2570,16 @@
     if (!block_pointer_input) {
         float wheel = GetMouseWheelMove();
         if (wheel != 0.0f) {
-            Canvas_Camera_Zoom_At(
-                p_camera,
-                GetMousePosition(),
-                powf(1.15f, wheel));
+            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});
+            }
         }
     }
 
@@ -1189,8 +2604,8 @@
     int32 last_x = (int32)ceilf((bounds.x + bounds.width) / spacing);
     int32 first_y = (int32)floorf(bounds.y / spacing);
     int32 last_y = (int32)ceilf((bounds.y + bounds.height) / spacing);
-    Color minor = Fade(p_theme->border, 0.36f);
-    Color major = Fade(p_theme->border, 0.65f);
+    Color 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++) {
@@ -1214,11 +2629,11 @@
 
 static void Canvas_Draw_Selection(
     const Canvas_Entity *p_entity,
-    float zoom)
+    float zoom,
+    Color selection)
 {
     float line = 2.0f / zoom;
     float padding = 7.0f / zoom;
-    Color selection = {0, 122, 255, 230};
 
     switch (p_entity->type) {
         case CANVAS_ENTITY_RECTANGLE:
@@ -1248,7 +2663,7 @@
                     p_entity->position.y + p_entity->size.y,
                 },
                 10.0f / zoom,
-                Fade(selection, 0.35f));
+                Canvas_Color_Fade(selection, 0.35f));
             break;
         case CANVAS_ENTITY_TEXT:
         case CANVAS_ENTITY_BUTTON:
@@ -1275,6 +2690,20 @@
                 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){
@@ -1296,7 +2725,7 @@
                     CANVAS_WEB_RESIZE_HANDLE / zoom,
                     CANVAS_WEB_RESIZE_HANDLE / zoom,
                 },
-                Fade(selection, 0.18f));
+                Canvas_Color_Fade(selection, 0.18f));
             DrawLineEx(
                 (Vector2){
                     p_entity->position.x + p_entity->size.x - 11.0f / zoom,
@@ -1315,6 +2744,124 @@
 
 }
 
+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,
@@ -1323,6 +2870,16 @@
     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[] = {
@@ -1352,7 +2909,7 @@
                 },
                 0.14f,
                 12,
-                Fade(p_entity->color, 0.92f));
+                Canvas_Color_Fade(p_entity->color, 0.92f));
             DrawRectangleRoundedLinesEx(
                 (Rectangle){
                     p_entity->position.x,
@@ -1363,7 +2920,7 @@
                 0.14f,
                 12,
                 1.0f / zoom,
-                Fade(BLACK, 0.08f));
+                p_theme->entity_outline);
             break;
         case CANVAS_ENTITY_CIRCLE:
             DrawCircleV(
@@ -1413,11 +2970,11 @@
                 p_entity->size.y + grow * 2.0f,
             };
             Color base_fill = p_entity->active ?
-                (Color){35, 99, 235, 255} :
-                (Color){17, 24, 39, 255};
+                p_theme->button_active :
+                p_theme->button;
             Color hover_fill = p_entity->active ?
-                (Color){59, 130, 246, 255} :
-                (Color){39, 48, 64, 255};
+                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,
@@ -1437,7 +2994,7 @@
                 },
                 16.0f,
                 0.1f,
-                WHITE);
+                p_theme->on_accent);
             break;
         }
         case CANVAS_ENTITY_TEXT_AREA: {
@@ -1455,19 +3012,7 @@
                 14,
                 (p_entity->active ? 2.0f : 1.0f) / zoom,
                 p_entity->active ? p_theme->accent : p_theme->border);
-            DrawTextEx(
-                font,
-                p_entity->text[0] ? p_entity->text : "Write something...",
-                (Vector2){bounds.x + 16.0f, bounds.y + 15.0f},
-                16.0f,
-                0.1f,
-                p_entity->text[0] ? p_theme->text : p_theme->text_muted);
-            if (p_entity->active) {
-                DrawCircleV(
-                    (Vector2){bounds.x + bounds.width - 16.0f, bounds.y + 16.0f},
-                    3.0f,
-                    p_theme->accent);
-            }
+            Canvas_Draw_Text_Area_Content(p_entity, font, p_theme);
             break;
         }
         case CANVAS_ENTITY_DROPDOWN: {
@@ -1552,7 +3097,7 @@
                     (Vector2){bounds.x, bounds.y + 56.0f},
                     (Vector2){bounds.x + bounds.width, bounds.y + 56.0f},
                     1.0f / zoom,
-                    Fade(p_theme->border, amount));
+                    Canvas_Color_Fade(p_theme->border, amount));
                 if (amount > 0.55f) {
                     float body_alpha = Canvas_Clamp(
                         (amount - 0.55f) / 0.45f,
@@ -1564,7 +3109,7 @@
                         (Vector2){bounds.x + 16.0f, bounds.y + 76.0f},
                         14.0f,
                         0.1f,
-                        Fade(p_theme->text_muted, body_alpha));
+                        Canvas_Color_Fade(p_theme->text_muted, body_alpha));
                 }
             }
             break;
@@ -1585,11 +3130,13 @@
                 12,
                 p_entity->active ?
                     p_theme->accent_soft :
-                    Fade(p_entity->color, 0.22f));
+                    Canvas_Color_Fade(p_entity->color, 0.22f));
             DrawCircleV(
                 (Vector2){bounds.x + 44.0f, bounds.y + 58.0f},
                 18.0f,
-                p_entity->active ? (Color){37, 99, 235, 255} : p_entity->color);
+                p_entity->active ?
+                    p_theme->accent :
+                    p_entity->color);
             DrawTextEx(
                 font,
                 p_entity->text,
@@ -1666,7 +3213,9 @@
                         center.y - label_size.y * 0.5f},
                     13.0f,
                     0.0f,
-                    day == p_entity->value ? WHITE : p_theme->text);
+                    day == p_entity->value ?
+                        p_theme->on_accent :
+                        p_theme->text);
             }
             break;
         }
@@ -1679,16 +3228,15 @@
                 p_entity->size.x,
                 p_entity->size.y,
             };
-            Color track = Canvas_Color_Lerp(
-                p_theme->border,
-                p_theme->accent,
-                eased);
-            if (!p_entity->active) {
-                track = Canvas_Color_Lerp(
-                    track,
-                    p_theme->text_muted,
-                    p_entity->hover_amount);
-            }
+            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 +
@@ -1697,7 +3245,7 @@
             DrawCircleV(
                 (Vector2){knob_x, bounds.y + bounds.height * 0.5f},
                 knob_radius,
-                WHITE);
+                p_theme->switch_thumb);
             break;
         }
         case CANVAS_ENTITY_TABLE: {
@@ -1784,27 +3332,27 @@
                 bounds,
                 0.20f,
                 16,
-                Fade(p_theme->toast_surface, opacity));
+                Canvas_Color_Fade(p_theme->toast_surface, opacity));
             DrawRectangleRoundedLinesEx(
                 bounds,
                 0.20f,
                 16,
                 1.0f / zoom,
-                Fade(p_theme->toast_border, opacity));
+                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,
-                Fade(p_theme->text, opacity));
+                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,
-                Fade(p_theme->text_muted, opacity));
+                Canvas_Color_Fade(p_theme->text_muted, opacity));
             Rectangle undo = {
                 bounds.x + bounds.width - 108.0f * unit,
                 bounds.y + 24.0f * unit,
@@ -1815,20 +3363,20 @@
                 undo,
                 0.26f,
                 10,
-                Fade(p_theme->surface_muted, opacity));
+                Canvas_Color_Fade(p_theme->surface_muted, opacity));
             DrawRectangleRoundedLinesEx(
                 undo,
                 0.26f,
                 10,
                 1.0f / zoom,
-                Fade(p_theme->border, opacity));
+                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,
-                Fade(p_theme->text, opacity));
+                Canvas_Color_Fade(p_theme->text, opacity));
             Vector2 close = {
                 bounds.x + bounds.width - 24.0f * unit,
                 bounds.y + bounds.height * 0.5f,
@@ -1837,12 +3385,12 @@
                 (Vector2){close.x - 4.0f * unit, close.y - 4.0f * unit},
                 (Vector2){close.x + 4.0f * unit, close.y + 4.0f * unit},
                 1.5f / zoom,
-                Fade(p_theme->text_muted, opacity));
+                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,
-                Fade(p_theme->text_muted, opacity));
+                Canvas_Color_Fade(p_theme->text_muted, opacity));
             break;
         }
         case CANVAS_ENTITY_SCROLL_AREA: {
@@ -1896,7 +3444,7 @@
                 (Rectangle){bounds.x + bounds.width - 14.0f, thumb_y, 5.0f, 42.0f},
                 0.50f,
                 6,
-                Fade(p_theme->text_muted, 0.82f));
+                Canvas_Color_Fade(p_theme->text_muted, 0.82f));
             break;
         }
         case CANVAS_ENTITY_IMAGE: {
@@ -1957,25 +3505,195 @@
 #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) {
-        float radius = 7.0f / zoom;
-        Vector2 center = {
-            p_entity->position.x + p_entity->size.x - 10.0f / zoom,
-            p_entity->position.y + 10.0f / zoom,
+        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,
         };
-        DrawCircleV(center, radius, p_theme->accent);
-        DrawLineEx(
-            (Vector2){center.x, center.y - 3.5f / zoom},
-            (Vector2){center.x, center.y + 4.0f / zoom},
-            1.5f / zoom,
-            WHITE);
+        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);
+        Canvas_Draw_Selection(p_entity, zoom, p_theme->selection);
     }
 }
 
@@ -1986,6 +3704,11 @@
     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",
@@ -2038,14 +3761,9 @@
     }
 }
 
-void Canvas_Draw_World(
-    const Canvas_Camera *p_camera,
-    const Canvas_Scene *p_scene,
-    Font font,
-    const Canvas_Theme *p_theme)
+static Camera2D Canvas_Raylib_Camera(const Canvas_Camera *p_camera)
 {
-    Vector2 world_pointer = Canvas_Camera_Screen_To_World(p_camera, GetMousePosition());
-    Camera2D camera = {
+    return (Camera2D){
         .offset = {
             (float)p_camera->viewport_width * 0.5f,
             (float)p_camera->viewport_height * 0.5f,
@@ -2054,18 +3772,229 @@
         .rotation = 0.0f,
         .zoom = p_camera->zoom,
     };
+}
 
-    BeginMode2D(camera);
+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);
-    for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
-        Canvas_Draw_Entity(
-            &p_scene->p_entities[index],
-            font,
-            p_camera->zoom,
-            (int32)index == p_scene->selected_index,
-            (int32)index == p_scene->pressed_index && IsMouseButtonDown(MOUSE_BUTTON_LEFT),
-            p_theme);
+    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) {
@@ -2080,6 +4009,24 @@
     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) {
@@ -2099,6 +4046,7 @@
         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";
     }
 }