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