Mercurial
view infinite_canvas/canvas.c @ 280:49e9e591c9bb
Add persistent dictation, prewarmed WebRTC speech input, Copilot SDK routing, animated conversation lifecycle controls, parking, and architecture coverage.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Tue, 18 Aug 2026 19:14:53 -0700 |
| parents | 8d560f50ed4c |
| children | c57149ad216e |
line wrap: on
line source
#include "infinite_canvas/canvas.h" #include "infinite_canvas/generated/lucide_data.h" #include <math.h> #include <stdarg.h> #include <stdio.h> #include <string.h> static const char *CANVAS_DROPDOWN_OPTIONS[] = { "Design", "Engineering", "Research", }; static const char *CANVAS_TABLE_NAMES[] = { "Canvas", "Browser", "Notes", "Assets", }; static const char *CANVAS_TABLE_STATUSES[] = { "Ready", "Live", "Draft", "Synced", }; #define CANVAS_LUCIDE_COLUMNS 10 #define CANVAS_LUCIDE_CARD_WIDTH 128.0f #define CANVAS_LUCIDE_CARD_HEIGHT 96.0f #define CANVAS_LUCIDE_GAP 12.0f #define CANVAS_LUCIDE_HEADER_HEIGHT 88.0f #define CANVAS_LUCIDE_PADDING 24.0f #define CANVAS_LUCIDE_SEARCH_WIDTH 360.0f #define CANVAS_LUCIDE_SEARCH_HEIGHT 42.0f #define CANVAS_TEXT_AREA_PADDING 16.0f #define CANVAS_TEXT_AREA_FONT_SIZE 16.0f #define CANVAS_TEXT_AREA_SPACING 0.1f #define CANVAS_TEXT_AREA_LINE_HEIGHT 22.0f #define CANVAS_CONVERSATION_EXPANDED_HEIGHT 300.0f #define CANVAS_CONVERSATION_COLLAPSED_HEIGHT 58.0f #define CANVAS_PARKING_LOT_X 2400.0f #define CANVAS_PARKING_LOT_Y -600.0f typedef struct { int32 start; int32 end; int32 next; float width; } Canvas_Text_Line; static int32 Canvas_Text_Build_Lines( Font font, const char *p_text, float max_width, Canvas_Text_Line *p_lines, int32 line_capacity); static float Canvas_Clamp(float value, float min_value, float max_value) { if (value < min_value) return min_value; if (value > max_value) return max_value; return value; } static Color Canvas_Entity_Color(uint32 index) { static const Color colors[] = { {96, 165, 250, 255}, {251, 191, 36, 255}, {167, 139, 250, 255}, {45, 212, 191, 255}, {251, 113, 133, 255}, }; return colors[index % (sizeof(colors) / sizeof(colors[0]))]; } static Color Canvas_Color_Lerp(Color from, Color to, float amount) { amount = Canvas_Clamp(amount, 0.0f, 1.0f); return (Color){ (uint8)((float)from.r + ((float)to.r - (float)from.r) * amount), (uint8)((float)from.g + ((float)to.g - (float)from.g) * amount), (uint8)((float)from.b + ((float)to.b - (float)from.b) * amount), (uint8)((float)from.a + ((float)to.a - (float)from.a) * amount), }; } static Color Canvas_Color_With_Opacity(Color color, float opacity) { color.a = (uint8)((float)color.a * Canvas_Clamp(opacity, 0.0f, 1.0f)); return color; } static Color Canvas_Color_Fade(Color color, float amount) { return Canvas_Color_With_Opacity(color, amount); } static float Canvas_Entity_Visibility(const Canvas_Entity *p_entity) { float amount = Canvas_Clamp(p_entity->visibility_amount, 0.0f, 1.0f); return amount * amount * (3.0f - 2.0f * amount); } static Canvas_Theme Canvas_Theme_With_Opacity( const Canvas_Theme *p_theme, float opacity) { Canvas_Theme faded = *p_theme; faded.background = Canvas_Color_With_Opacity(faded.background, opacity); faded.surface = Canvas_Color_With_Opacity(faded.surface, opacity); faded.surface_muted = Canvas_Color_With_Opacity(faded.surface_muted, opacity); faded.border = Canvas_Color_With_Opacity(faded.border, opacity); faded.text = Canvas_Color_With_Opacity(faded.text, opacity); faded.text_muted = Canvas_Color_With_Opacity(faded.text_muted, opacity); faded.accent = Canvas_Color_With_Opacity(faded.accent, opacity); faded.accent_soft = Canvas_Color_With_Opacity(faded.accent_soft, opacity); faded.on_accent = Canvas_Color_With_Opacity(faded.on_accent, opacity); faded.selection = Canvas_Color_With_Opacity(faded.selection, opacity); faded.entity_outline = Canvas_Color_With_Opacity(faded.entity_outline, opacity); faded.button = Canvas_Color_With_Opacity(faded.button, opacity); faded.button_hover = Canvas_Color_With_Opacity(faded.button_hover, opacity); faded.button_active = Canvas_Color_With_Opacity(faded.button_active, opacity); faded.button_active_hover = Canvas_Color_With_Opacity( faded.button_active_hover, opacity); faded.switch_track_off = Canvas_Color_With_Opacity( faded.switch_track_off, opacity); faded.switch_track_off_hover = Canvas_Color_With_Opacity( faded.switch_track_off_hover, opacity); faded.switch_track_on = Canvas_Color_With_Opacity( faded.switch_track_on, opacity); faded.switch_track_on_hover = Canvas_Color_With_Opacity( faded.switch_track_on_hover, opacity); faded.switch_thumb = Canvas_Color_With_Opacity(faded.switch_thumb, opacity); faded.pin = Canvas_Color_With_Opacity(faded.pin, opacity); faded.danger = Canvas_Color_With_Opacity(faded.danger, opacity); faded.shadow = Canvas_Color_With_Opacity(faded.shadow, opacity); faded.toast_surface = Canvas_Color_With_Opacity(faded.toast_surface, opacity); faded.toast_border = Canvas_Color_With_Opacity(faded.toast_border, opacity); return faded; } static boolean Canvas_Control_Is_Down(void) { return IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL) || IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_RIGHT_SUPER); } static boolean Canvas_Shift_Is_Down(void); static boolean Canvas_Key_Activated(int32 key); static char Canvas_Ascii_Lower(char value) { if (value >= 'A' && value <= 'Z') return (char)(value + ('a' - 'A')); return value; } static boolean Canvas_Lucide_Name_Matches( const char *p_name, const char *p_query) { if (!p_query || !p_query[0]) return TRUE; for (const char *p_start = p_name; *p_start; p_start++) { const char *p_name_cursor = p_start; const char *p_query_cursor = p_query; while (*p_name_cursor && *p_query_cursor && Canvas_Ascii_Lower(*p_name_cursor) == Canvas_Ascii_Lower(*p_query_cursor)) { p_name_cursor++; p_query_cursor++; } if (!*p_query_cursor) return TRUE; } return FALSE; } int32 Canvas_Lucide_Count_Matches(const char *p_query) { int32 count = 0; for (int32 index = 0; index < CANVAS_LUCIDE_ICON_COUNT; index++) { if (Canvas_Lucide_Name_Matches( CANVAS_LUCIDE_ICONS[index].p_name, p_query)) { count++; } } return count; } static Rectangle Canvas_Lucide_Search_Bounds(const Canvas_Entity *p_entity) { return (Rectangle){ p_entity->position.x + p_entity->size.x - CANVAS_LUCIDE_PADDING - CANVAS_LUCIDE_SEARCH_WIDTH, p_entity->position.y + (CANVAS_LUCIDE_HEADER_HEIGHT - CANVAS_LUCIDE_SEARCH_HEIGHT) * 0.5f, CANVAS_LUCIDE_SEARCH_WIDTH, CANVAS_LUCIDE_SEARCH_HEIGHT, }; } static float Canvas_Lucide_Content_Height(const char *p_query) { int32 icon_count = Canvas_Lucide_Count_Matches(p_query); int32 rows = (icon_count + CANVAS_LUCIDE_COLUMNS - 1) / CANVAS_LUCIDE_COLUMNS; if (rows == 0) { return CANVAS_LUCIDE_HEADER_HEIGHT + CANVAS_LUCIDE_PADDING * 2.0f; } return CANVAS_LUCIDE_HEADER_HEIGHT + CANVAS_LUCIDE_PADDING + (float)rows * CANVAS_LUCIDE_CARD_HEIGHT + (float)(rows - 1) * CANVAS_LUCIDE_GAP + CANVAS_LUCIDE_PADDING; } static float Canvas_Lucide_Max_Scroll(const Canvas_Entity *p_entity) { return fmaxf( 0.0f, Canvas_Lucide_Content_Height(p_entity->text) - p_entity->size.y); } static const Canvas_Lucide_Icon *Canvas_Lucide_Find_Icon( const char *p_name) { for (int32 index = 0; index < CANVAS_LUCIDE_ICON_COUNT; index++) { if (strcmp(CANVAS_LUCIDE_ICONS[index].p_name, p_name) == 0) { return &CANVAS_LUCIDE_ICONS[index]; } } return NULL; } static void Canvas_Lucide_Draw_Icon( const Canvas_Lucide_Icon *p_icon, Vector2 origin, float scale, float line_thickness, Color color) { if (!p_icon) return; for (uint32 segment_index = p_icon->first_segment; segment_index < p_icon->first_segment + p_icon->segment_count; segment_index++) { const Canvas_Lucide_Segment *p_segment = &CANVAS_LUCIDE_SEGMENTS[segment_index]; DrawLineEx( (Vector2){ origin.x + p_segment->x1 * scale, origin.y + p_segment->y1 * scale, }, (Vector2){ origin.x + p_segment->x2 * scale, origin.y + p_segment->y2 * scale, }, line_thickness, color); } } static Rectangle Canvas_Web_Toolbar_Bounds( const Canvas_Camera *p_camera, const Canvas_Entity *p_entity) { float zoom = p_camera->zoom; return (Rectangle){ p_entity->position.x + CANVAS_WEB_FRAME_INSET / zoom, p_entity->position.y + CANVAS_WEB_FRAME_INSET / zoom, p_entity->size.x - (CANVAS_WEB_FRAME_INSET + CANVAS_WEB_RESIZE_HANDLE) / zoom, CANVAS_WEB_TOOLBAR_HEIGHT * (p_entity->web_chrome_amount * p_entity->web_chrome_amount * (3.0f - 2.0f * p_entity->web_chrome_amount)) / zoom, }; } static Rectangle Canvas_Web_Toolbar_Button_Bounds( Rectangle toolbar, float zoom, int32 button_index) { return (Rectangle){ toolbar.x + (6.0f + (float)button_index * 34.0f) / zoom, toolbar.y + 7.0f / zoom, 28.0f / zoom, 28.0f / zoom, }; } static Rectangle Canvas_Web_Address_Bounds(Rectangle toolbar, float zoom) { return (Rectangle){ toolbar.x + 76.0f / zoom, toolbar.y + 7.0f / zoom, toolbar.width - 82.0f / zoom, 28.0f / zoom, }; } static void Canvas_Web_URL_Selection_Bounds( const Canvas_Scene *p_scene, int32 *p_start, int32 *p_end) { *p_start = p_scene->web_url_cursor; *p_end = p_scene->web_url_selection_anchor; if (*p_start > *p_end) { int32 swap = *p_start; *p_start = *p_end; *p_end = swap; } } static void Canvas_Web_URL_Delete_Selection(Canvas_Scene *p_scene) { int32 start = 0; int32 end = 0; Canvas_Web_URL_Selection_Bounds(p_scene, &start, &end); if (start == end) return; size_t length = strlen(p_scene->web_url_draft); memmove( p_scene->web_url_draft + start, p_scene->web_url_draft + end, length - (size_t)end + 1); p_scene->web_url_cursor = start; p_scene->web_url_selection_anchor = start; } static void Canvas_Web_URL_Insert(Canvas_Scene *p_scene, const char *p_text) { Canvas_Web_URL_Delete_Selection(p_scene); size_t length = strlen(p_scene->web_url_draft); size_t available = sizeof(p_scene->web_url_draft) - length - 1; if (available == 0) return; char filtered[CANVAS_ENTITY_TEXT_CAPACITY]; size_t filtered_length = 0; for (const char *p_cursor = p_text; *p_cursor && filtered_length < available; p_cursor++) { if (*p_cursor >= 32 && *p_cursor <= 126) { filtered[filtered_length++] = *p_cursor; } } if (filtered_length == 0) return; int32 cursor = p_scene->web_url_cursor; memmove( p_scene->web_url_draft + cursor + filtered_length, p_scene->web_url_draft + cursor, length - (size_t)cursor + 1); memcpy(p_scene->web_url_draft + cursor, filtered, filtered_length); p_scene->web_url_cursor += (int32)filtered_length; p_scene->web_url_selection_anchor = p_scene->web_url_cursor; } static int32 Canvas_Web_URL_Cursor_At_Point( Font font, const Canvas_Scene *p_scene, Rectangle address, float zoom, Vector2 point) { float font_size = 13.0f / zoom; float local_x = point.x - address.x - 9.0f / zoom + p_scene->web_url_scroll_x; int32 length = (int32)strlen(p_scene->web_url_draft); for (int32 cursor = 0; cursor < length; cursor++) { char prefix[CANVAS_ENTITY_TEXT_CAPACITY]; memcpy(prefix, p_scene->web_url_draft, (size_t)cursor + 1); prefix[cursor + 1] = '\0'; float right = MeasureTextEx(font, prefix, font_size, 0.0f).x; prefix[cursor] = '\0'; float left = MeasureTextEx(font, prefix, font_size, 0.0f).x; if (local_x < (left + right) * 0.5f) return cursor; } return length; } static void Canvas_Web_URL_Ensure_Cursor_Visible( Canvas_Scene *p_scene, Font font, Rectangle address, float zoom) { char prefix[CANVAS_ENTITY_TEXT_CAPACITY]; memcpy( prefix, p_scene->web_url_draft, (size_t)p_scene->web_url_cursor); prefix[p_scene->web_url_cursor] = '\0'; float cursor_x = MeasureTextEx(font, prefix, 13.0f / zoom, 0.0f).x; float visible_width = address.width - 18.0f / zoom; if (cursor_x < p_scene->web_url_scroll_x) { p_scene->web_url_scroll_x = cursor_x; } else if (cursor_x > p_scene->web_url_scroll_x + visible_width) { p_scene->web_url_scroll_x = cursor_x - visible_width; } if (p_scene->web_url_scroll_x < 0.0f) p_scene->web_url_scroll_x = 0.0f; } static void Canvas_Web_URL_Copy_Selection(Canvas_Scene *p_scene) { int32 start = 0; int32 end = 0; Canvas_Web_URL_Selection_Bounds(p_scene, &start, &end); if (start == end) return; char selected[CANVAS_ENTITY_TEXT_CAPACITY]; memcpy( selected, p_scene->web_url_draft + start, (size_t)(end - start)); selected[end - start] = '\0'; SetClipboardText(selected); } static void Canvas_Web_URL_Normalize(char *p_url, size_t capacity) { if (!p_url[0] || strstr(p_url, "://") || strncmp(p_url, "about:", 6) == 0 || strncmp(p_url, "data:", 5) == 0 || strncmp(p_url, "file:", 5) == 0) { return; } char normalized[CANVAS_ENTITY_TEXT_CAPACITY]; snprintf(normalized, sizeof(normalized), "https://%s", p_url); snprintf(p_url, capacity, "%s", normalized); } Canvas_Web_Chrome_Action Canvas_Web_Chrome_Update_Input( const Canvas_Camera *p_camera, Canvas_Scene *p_scene, Font font, const char *p_current_url) { if (p_scene->selected_index < 0 || p_scene->selected_index >= (int32)Dowa_Array_Length(p_scene->p_entities)) { p_scene->editing_web_url = FALSE; return CANVAS_WEB_CHROME_NONE; } Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index]; if (p_entity->type != CANVAS_ENTITY_WEB_CONTENT || p_entity->removing) { p_scene->editing_web_url = FALSE; return CANVAS_WEB_CHROME_NONE; } Rectangle toolbar = Canvas_Web_Toolbar_Bounds(p_camera, p_entity); Rectangle address = Canvas_Web_Address_Bounds(toolbar, p_camera->zoom); Vector2 pointer = Canvas_Camera_Screen_To_World( p_camera, GetMousePosition()); if (p_scene->editing_web_url && p_scene->web_url_entity_id == p_entity->id) { boolean shortcut = Canvas_Control_Is_Down(); boolean extend = Canvas_Shift_Is_Down(); int32 length = (int32)strlen(p_scene->web_url_draft); if (shortcut && IsKeyPressed(KEY_A)) { p_scene->web_url_selection_anchor = 0; p_scene->web_url_cursor = length; } if (shortcut && IsKeyPressed(KEY_C)) { Canvas_Web_URL_Copy_Selection(p_scene); } if (shortcut && IsKeyPressed(KEY_X)) { Canvas_Web_URL_Copy_Selection(p_scene); Canvas_Web_URL_Delete_Selection(p_scene); } if (shortcut && IsKeyPressed(KEY_V)) { const char *p_clipboard = GetClipboardText(); if (p_clipboard) Canvas_Web_URL_Insert(p_scene, p_clipboard); } if (Canvas_Key_Activated(KEY_LEFT)) { int32 cursor = p_scene->web_url_cursor; if (!extend && cursor != p_scene->web_url_selection_anchor) { int32 end = 0; Canvas_Web_URL_Selection_Bounds(p_scene, &cursor, &end); } else { cursor--; } if (cursor < 0) cursor = 0; p_scene->web_url_cursor = cursor; if (!extend) p_scene->web_url_selection_anchor = cursor; } if (Canvas_Key_Activated(KEY_RIGHT)) { int32 cursor = p_scene->web_url_cursor; if (!extend && cursor != p_scene->web_url_selection_anchor) { int32 start = 0; Canvas_Web_URL_Selection_Bounds(p_scene, &start, &cursor); } else { cursor++; } if (cursor > length) cursor = length; p_scene->web_url_cursor = cursor; if (!extend) p_scene->web_url_selection_anchor = cursor; } if (Canvas_Key_Activated(KEY_HOME)) { p_scene->web_url_cursor = 0; if (!extend) p_scene->web_url_selection_anchor = 0; } if (Canvas_Key_Activated(KEY_END)) { p_scene->web_url_cursor = length; if (!extend) p_scene->web_url_selection_anchor = length; } if (Canvas_Key_Activated(KEY_BACKSPACE)) { if (p_scene->web_url_cursor != p_scene->web_url_selection_anchor) { Canvas_Web_URL_Delete_Selection(p_scene); } else if (p_scene->web_url_cursor > 0) { p_scene->web_url_selection_anchor = p_scene->web_url_cursor - 1; Canvas_Web_URL_Delete_Selection(p_scene); } } if (Canvas_Key_Activated(KEY_DELETE)) { if (p_scene->web_url_cursor != p_scene->web_url_selection_anchor) { Canvas_Web_URL_Delete_Selection(p_scene); } else if (p_scene->web_url_cursor < length) { p_scene->web_url_selection_anchor = p_scene->web_url_cursor + 1; Canvas_Web_URL_Delete_Selection(p_scene); } } int32 codepoint = GetCharPressed(); while (codepoint > 0) { if (!shortcut && codepoint >= 32 && codepoint <= 126) { char inserted[2] = {(char)codepoint, '\0'}; Canvas_Web_URL_Insert(p_scene, inserted); } codepoint = GetCharPressed(); } Canvas_Web_URL_Ensure_Cursor_Visible( p_scene, font, address, p_camera->zoom); if (IsKeyPressed(KEY_ESCAPE)) { p_scene->editing_web_url = FALSE; } if (IsKeyPressed(KEY_ENTER)) { Canvas_Web_URL_Normalize( p_scene->web_url_draft, sizeof(p_scene->web_url_draft)); snprintf( p_entity->text, sizeof(p_entity->text), "%s", p_scene->web_url_draft); p_scene->editing_web_url = FALSE; return CANVAS_WEB_CHROME_NAVIGATE; } } if (!CheckCollisionPointRec(pointer, toolbar)) { if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { p_scene->editing_web_url = FALSE; return CANVAS_WEB_CHROME_NONE; } return p_scene->editing_web_url ? CANVAS_WEB_CHROME_CAPTURE : CANVAS_WEB_CHROME_NONE; } if (!IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { return CANVAS_WEB_CHROME_CAPTURE; } if (CheckCollisionPointRec( pointer, Canvas_Web_Toolbar_Button_Bounds(toolbar, p_camera->zoom, 0))) { return CANVAS_WEB_CHROME_BACK; } if (CheckCollisionPointRec( pointer, Canvas_Web_Toolbar_Button_Bounds(toolbar, p_camera->zoom, 1))) { return CANVAS_WEB_CHROME_FORWARD; } if (CheckCollisionPointRec(pointer, address)) { if (!p_scene->editing_web_url || p_scene->web_url_entity_id != p_entity->id) { snprintf( p_scene->web_url_draft, sizeof(p_scene->web_url_draft), "%s", p_current_url && p_current_url[0] ? p_current_url : p_entity->text); p_scene->web_url_entity_id = p_entity->id; p_scene->web_url_cursor = (int32)strlen(p_scene->web_url_draft); p_scene->web_url_selection_anchor = 0; p_scene->web_url_scroll_x = 0.0f; p_scene->editing_web_url = TRUE; } else { int32 cursor = Canvas_Web_URL_Cursor_At_Point( font, p_scene, address, p_camera->zoom, pointer); p_scene->web_url_cursor = cursor; if (!Canvas_Shift_Is_Down()) { p_scene->web_url_selection_anchor = cursor; } } Canvas_Web_URL_Ensure_Cursor_Visible( p_scene, font, address, p_camera->zoom); return CANVAS_WEB_CHROME_CAPTURE; } p_scene->editing_web_url = FALSE; return CANVAS_WEB_CHROME_CAPTURE; } boolean Canvas_Web_Chrome_Is_Editing(const Canvas_Scene *p_scene) { return p_scene->editing_web_url; } void Canvas_Camera_Init(Canvas_Camera *p_camera, int32 width, int32 height) { *p_camera = (Canvas_Camera){ .target = {0.0f, 0.0f}, .zoom = 1.0f, .viewport_width = width, .viewport_height = height, .min_zoom = 0.01f, .max_zoom = 64.0f, }; } void Canvas_Camera_Set_Viewport(Canvas_Camera *p_camera, int32 width, int32 height) { p_camera->viewport_width = width; p_camera->viewport_height = height; } Vector2 Canvas_Camera_Screen_To_World(const Canvas_Camera *p_camera, Vector2 screen) { Vector2 center = { (float)p_camera->viewport_width * 0.5f, (float)p_camera->viewport_height * 0.5f, }; return (Vector2){ p_camera->target.x + (screen.x - center.x) / p_camera->zoom, p_camera->target.y + (screen.y - center.y) / p_camera->zoom, }; } Vector2 Canvas_Camera_World_To_Screen(const Canvas_Camera *p_camera, Vector2 world) { Vector2 center = { (float)p_camera->viewport_width * 0.5f, (float)p_camera->viewport_height * 0.5f, }; return (Vector2){ center.x + (world.x - p_camera->target.x) * p_camera->zoom, center.y + (world.y - p_camera->target.y) * p_camera->zoom, }; } void Canvas_Camera_Pan(Canvas_Camera *p_camera, Vector2 screen_delta) { p_camera->target.x -= screen_delta.x / p_camera->zoom; p_camera->target.y -= screen_delta.y / p_camera->zoom; } void Canvas_Camera_Zoom_At( Canvas_Camera *p_camera, Vector2 screen_anchor, float zoom_multiplier) { Vector2 world_anchor = Canvas_Camera_Screen_To_World(p_camera, screen_anchor); p_camera->zoom = Canvas_Clamp( p_camera->zoom * zoom_multiplier, p_camera->min_zoom, p_camera->max_zoom); Vector2 moved_anchor = Canvas_Camera_Screen_To_World(p_camera, screen_anchor); p_camera->target.x += world_anchor.x - moved_anchor.x; p_camera->target.y += world_anchor.y - moved_anchor.y; } Rectangle Canvas_Camera_Viewport_Bounds(const Canvas_Camera *p_camera) { Vector2 min = Canvas_Camera_Screen_To_World(p_camera, (Vector2){0.0f, 0.0f}); Vector2 max = Canvas_Camera_Screen_To_World( p_camera, (Vector2){(float)p_camera->viewport_width, (float)p_camera->viewport_height}); return (Rectangle){ .x = min.x, .y = min.y, .width = max.x - min.x, .height = max.y - min.y, }; } void Canvas_Camera_Fit_Bounds( Canvas_Camera *p_camera, Rectangle bounds, float padding_left, float padding_top, float padding_right, float padding_bottom) { float available_width = fmaxf( 1.0f, (float)p_camera->viewport_width - padding_left - padding_right); float available_height = fmaxf( 1.0f, (float)p_camera->viewport_height - padding_top - padding_bottom); float zoom = fminf( available_width / fmaxf(bounds.width, 1.0f), available_height / fmaxf(bounds.height, 1.0f)); p_camera->zoom = Canvas_Clamp( zoom, p_camera->min_zoom, p_camera->max_zoom); Vector2 world_center = { bounds.x + bounds.width * 0.5f, bounds.y + bounds.height * 0.5f, }; Vector2 desired_screen_center = { padding_left + available_width * 0.5f, padding_top + available_height * 0.5f, }; Vector2 viewport_center = { (float)p_camera->viewport_width * 0.5f, (float)p_camera->viewport_height * 0.5f, }; p_camera->target = (Vector2){ world_center.x - (desired_screen_center.x - viewport_center.x) / p_camera->zoom, world_center.y - (desired_screen_center.y - viewport_center.y) / p_camera->zoom, }; } static Rectangle Canvas_Conversation_Collapse_Bounds( const Canvas_Entity *p_entity) { return (Rectangle){ p_entity->position.x + p_entity->size.x - 74.0f, p_entity->position.y + 12.0f, 28.0f, 28.0f, }; } static Rectangle Canvas_Conversation_Park_Bounds( const Canvas_Entity *p_entity) { return (Rectangle){ p_entity->position.x + p_entity->size.x - 40.0f, p_entity->position.y + 12.0f, 28.0f, 28.0f, }; } static Rectangle Canvas_Entity_Bounds(const Canvas_Entity *p_entity) { if (p_entity->type == CANVAS_ENTITY_CIRCLE) { return (Rectangle){ p_entity->position.x - p_entity->size.x, p_entity->position.y - p_entity->size.x, p_entity->size.x * 2.0f, p_entity->size.x * 2.0f, }; } if (p_entity->type == CANVAS_ENTITY_LINE) { float end_x = p_entity->position.x + p_entity->size.x; float end_y = p_entity->position.y + p_entity->size.y; return (Rectangle){ fminf(p_entity->position.x, end_x), fminf(p_entity->position.y, end_y), fmaxf(fabsf(p_entity->size.x), 0.001f), fmaxf(fabsf(p_entity->size.y), 0.001f), }; } Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; if (p_entity->type == CANVAS_ENTITY_DROPDOWN && p_entity->active) { bounds.height += 128.0f; } return bounds; } typedef struct { char *p_buffer; size_t capacity; size_t length; boolean truncated; } Canvas_Context_Writer; static void Canvas_Context_Append( Canvas_Context_Writer *p_writer, const char *p_format, ...) { if (p_writer->truncated || p_writer->capacity == 0) { p_writer->truncated = TRUE; return; } va_list arguments; va_start(arguments, p_format); int32 written = vsnprintf( p_writer->p_buffer + p_writer->length, p_writer->capacity - p_writer->length, p_format, arguments); va_end(arguments); if (written < 0) { p_writer->truncated = TRUE; return; } size_t available = p_writer->capacity - p_writer->length; if ((size_t)written >= available) { p_writer->length = p_writer->capacity - 1; p_writer->truncated = TRUE; return; } p_writer->length += (size_t)written; } static void Canvas_Context_Append_Quoted( Canvas_Context_Writer *p_writer, const char *p_text) { Canvas_Context_Append(p_writer, "\""); for (const char *p_cursor = p_text; *p_cursor && !p_writer->truncated; p_cursor++) { if (*p_cursor == '"' || *p_cursor == '\\') { Canvas_Context_Append(p_writer, "\\%c", *p_cursor); } else if (*p_cursor == '\n') { Canvas_Context_Append(p_writer, "\\n"); } else if (*p_cursor == '\r') { continue; } else { Canvas_Context_Append(p_writer, "%c", *p_cursor); } } Canvas_Context_Append(p_writer, "\""); } static void Canvas_Context_Append_Entity_State( Canvas_Context_Writer *p_writer, const Canvas_Entity *p_entity) { switch (p_entity->type) { case CANVAS_ENTITY_RECTANGLE: case CANVAS_ENTITY_CIRCLE: case CANVAS_ENTITY_LINE: Canvas_Context_Append( p_writer, "color=rgba(%u,%u,%u,%u)", p_entity->color.r, p_entity->color.g, p_entity->color.b, p_entity->color.a); break; case CANVAS_ENTITY_TEXT: Canvas_Context_Append(p_writer, "content="); Canvas_Context_Append_Quoted(p_writer, p_entity->text); break; case CANVAS_ENTITY_BUTTON: Canvas_Context_Append(p_writer, "label="); Canvas_Context_Append_Quoted(p_writer, p_entity->text); Canvas_Context_Append( p_writer, " activated=%s", p_entity->active ? "true" : "false"); break; case CANVAS_ENTITY_TEXT_AREA: Canvas_Context_Append(p_writer, "content="); Canvas_Context_Append_Quoted(p_writer, p_entity->text); Canvas_Context_Append( p_writer, " editing=%s cursor=%d selection_anchor=%d", p_entity->active ? "true" : "false", p_entity->text_cursor, p_entity->text_selection_anchor); break; case CANVAS_ENTITY_DROPDOWN: Canvas_Context_Append(p_writer, "selected="); Canvas_Context_Append_Quoted( p_writer, CANVAS_DROPDOWN_OPTIONS[p_entity->value]); Canvas_Context_Append( p_writer, " open=%s", p_entity->active ? "true" : "false"); break; case CANVAS_ENTITY_ACCORDION: Canvas_Context_Append(p_writer, "title="); Canvas_Context_Append_Quoted(p_writer, p_entity->text); Canvas_Context_Append( p_writer, " expanded=%s", p_entity->active ? "true" : "false"); break; case CANVAS_ENTITY_CARD: Canvas_Context_Append(p_writer, "title="); Canvas_Context_Append_Quoted(p_writer, p_entity->text); Canvas_Context_Append( p_writer, " emphasized=%s", p_entity->active ? "true" : "false"); break; case CANVAS_ENTITY_CALENDAR: Canvas_Context_Append( p_writer, "selected_date=\"2026-08-%02d\"", p_entity->value); break; case CANVAS_ENTITY_SWITCH: Canvas_Context_Append( p_writer, "checked=%s", p_entity->active ? "true" : "false"); break; case CANVAS_ENTITY_TABLE: Canvas_Context_Append(p_writer, "selected_row={name="); Canvas_Context_Append_Quoted( p_writer, CANVAS_TABLE_NAMES[p_entity->value]); Canvas_Context_Append(p_writer, ", status="); Canvas_Context_Append_Quoted( p_writer, CANVAS_TABLE_STATUSES[p_entity->value]); Canvas_Context_Append(p_writer, "}"); break; case CANVAS_ENTITY_NOTIFICATION: Canvas_Context_Append(p_writer, "title="); Canvas_Context_Append_Quoted(p_writer, p_entity->text); Canvas_Context_Append( p_writer, " active=%s", p_entity->active ? "true" : "false"); break; case CANVAS_ENTITY_SCROLL_AREA: { int32 first_item = p_entity->value / 42 + 1; int32 last_item = first_item + 3; if (last_item > 10) last_item = 10; Canvas_Context_Append( p_writer, "scroll_offset=%d visible_items=%d-%d", p_entity->value, first_item, last_item); break; } case CANVAS_ENTITY_IMAGE: Canvas_Context_Append(p_writer, "source="); Canvas_Context_Append_Quoted(p_writer, p_entity->text); break; case CANVAS_ENTITY_WEB_CONTENT: Canvas_Context_Append(p_writer, "url="); Canvas_Context_Append_Quoted(p_writer, p_entity->text); break; case CANVAS_ENTITY_CONVERSATION: Canvas_Context_Append(p_writer, "title="); Canvas_Context_Append_Quoted(p_writer, p_entity->label); Canvas_Context_Append(p_writer, " transcript="); Canvas_Context_Append_Quoted(p_writer, p_entity->text); Canvas_Context_Append( p_writer, " collapsed=%s", p_entity->active ? "true" : "false"); break; case CANVAS_ENTITY_LUCIDE_GALLERY: Canvas_Context_Append( p_writer, "icons=%d query=", Canvas_Lucide_Count_Matches(p_entity->text)); Canvas_Context_Append_Quoted(p_writer, p_entity->text); Canvas_Context_Append( p_writer, " search_focused=%s scroll_offset=%d renderer=\"raylib\"", p_entity->active ? "true" : "false", p_entity->value); break; default: Canvas_Context_Append(p_writer, "value=%d", p_entity->value); break; } } Canvas_Context_Snapshot Canvas_Scene_Build_Visible_Context( const Canvas_Scene *p_scene, const Canvas_Camera *p_camera, char *p_buffer, size_t buffer_size) { Canvas_Context_Writer writer = { .p_buffer = p_buffer, .capacity = buffer_size, }; if (buffer_size > 0) p_buffer[0] = '\0'; Rectangle viewport = Canvas_Camera_Viewport_Bounds(p_camera); Canvas_Context_Append( &writer, "camera_context zoom=%.2f viewport=[%.1f, %.1f, %.1f, %.1f]\n" "entities:\n", p_camera->zoom, viewport.x, viewport.y, viewport.width, viewport.height); Canvas_Context_Snapshot snapshot = {0}; for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { const Canvas_Entity *p_entity = &p_scene->p_entities[index]; if (p_entity->type == CANVAS_ENTITY_NOTIFICATION && p_entity->animation_amount <= 0.01f) { continue; } if (p_entity->type == CANVAS_ENTITY_TEXT_AREA && strcmp(p_entity->label, "Dictation") == 0) { continue; } if (p_entity->type == CANVAS_ENTITY_CONVERSATION && p_entity->parked) { continue; } if (!CheckCollisionRecs(viewport, Canvas_Entity_Bounds(p_entity))) continue; snapshot.visible_count++; Canvas_Context_Append( &writer, "- id=%u type=\"%s\" position=[%.1f, %.1f] size=[%.1f, %.1f] pinned=%s", p_entity->id, Canvas_Entity_Type_Name(p_entity->type), p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, p_entity->pinned ? "true" : "false"); Canvas_Context_Append( &writer, " visibility=%.2f removing=%s\n" " state: ", p_entity->visibility_amount, p_entity->removing ? "true" : "false"); Canvas_Context_Append_Entity_State(&writer, p_entity); Canvas_Context_Append(&writer, "\n"); } if (snapshot.visible_count == 0) { Canvas_Context_Append(&writer, " (none)\n"); } snapshot.bytes_written = writer.length; snapshot.truncated = writer.truncated; return snapshot; } float Canvas_Grid_Spacing(float zoom) { float spacing = 64.0f; while ((spacing * zoom < 48.0f) && (spacing < 1048576.0f)) spacing *= 2.0f; while ((spacing * zoom > 96.0f) && (spacing > 0.0009765625f)) spacing *= 0.5f; return spacing; } void Canvas_Scene_Init(Canvas_Scene *p_scene, Dowa_Arena *p_arena) { memset(p_scene, 0, sizeof(*p_scene)); p_scene->p_arena = p_arena; p_scene->selected_index = -1; p_scene->pressed_index = -1; p_scene->hover_index = -1; p_scene->show_grid = FALSE; Dowa_Array_Reserve_Arena(p_scene->p_entities, CANVAS_MAX_ENTITIES, p_arena); } boolean Canvas_Scene_Add(Canvas_Scene *p_scene, Canvas_Entity_Type type, Vector2 position) { if (Dowa_Array_Length(p_scene->p_entities) >= CANVAS_MAX_ENTITIES) return FALSE; uint32 index = p_scene->next_entity++; Canvas_Entity entity = { .id = ++p_scene->next_entity_id, .type = type, .position = position, .size = {160.0f, 100.0f}, .color = Canvas_Entity_Color(index), .text = {0}, .value = 0, .visibility_amount = 0.0f, .active = FALSE, .pinned = FALSE, .removing = FALSE, .hover_amount = 0.0f, .animation_amount = 0.0f, }; if (type == CANVAS_ENTITY_CIRCLE) entity.size = (Vector2){56.0f, 56.0f}; if (type == CANVAS_ENTITY_LINE) entity.size = (Vector2){180.0f, 80.0f}; if (type == CANVAS_ENTITY_TEXT) { entity.size = (Vector2){180.0f, 32.0f}; snprintf(entity.text, sizeof(entity.text), "Entity %u", index + 1); } if (type == CANVAS_ENTITY_BUTTON) { entity.size = (Vector2){156.0f, 48.0f}; snprintf(entity.text, sizeof(entity.text), "Continue"); } if (type == CANVAS_ENTITY_TEXT_AREA) { entity.size = (Vector2){280.0f, 148.0f}; } if (type == CANVAS_ENTITY_DROPDOWN) { entity.size = (Vector2){224.0f, 48.0f}; } if (type == CANVAS_ENTITY_ACCORDION) { entity.size = (Vector2){300.0f, 56.0f}; snprintf(entity.text, sizeof(entity.text), "How does the canvas work?"); } if (type == CANVAS_ENTITY_CARD) { entity.size = (Vector2){280.0f, 220.0f}; snprintf(entity.text, sizeof(entity.text), "Infinite workspace"); } if (type == CANVAS_ENTITY_CALENDAR) { entity.size = (Vector2){280.0f, 300.0f}; entity.value = 17; } if (type == CANVAS_ENTITY_SWITCH) { entity.size = (Vector2){64.0f, 36.0f}; } if (type == CANVAS_ENTITY_TABLE) { entity.size = (Vector2){420.0f, 230.0f}; entity.value = 1; } if (type == CANVAS_ENTITY_NOTIFICATION) { entity.size = (Vector2){384.0f, 82.0f}; entity.active = TRUE; entity.animation_amount = 1.0f; snprintf(entity.text, sizeof(entity.text), "Event created"); } if (type == CANVAS_ENTITY_SCROLL_AREA) { entity.size = (Vector2){320.0f, 220.0f}; } if (type == CANVAS_ENTITY_IMAGE) { entity.size = (Vector2){360.0f, 240.0f}; snprintf( entity.text, sizeof(entity.text), "infinite_canvas/assets/image-placeholder.svg"); } if (type == CANVAS_ENTITY_WEB_CONTENT) { entity.size = (Vector2){720.0f, 460.0f}; entity.active = TRUE; snprintf(entity.text, sizeof(entity.text), "https://mrjunejune.com"); } if (type == CANVAS_ENTITY_CONVERSATION) { entity.size = (Vector2){ 320.0f, CANVAS_CONVERSATION_EXPANDED_HEIGHT, }; entity.animation_amount = 1.0f; snprintf(entity.label, sizeof(entity.label), "Agent session"); } 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; } uint32 Canvas_Scene_Add_Conversation( Canvas_Scene *p_scene, const char *p_title, const char *p_prompt, const char *p_response, Vector2 position) { if (!Canvas_Scene_Add( p_scene, CANVAS_ENTITY_CONVERSATION, position)) { return 0; } Canvas_Entity *p_entity = &p_scene->p_entities[Dowa_Array_Length(p_scene->p_entities) - 1]; p_entity->size = (Vector2){420.0f, 300.0f}; snprintf( p_entity->label, sizeof(p_entity->label), "%s", p_title && p_title[0] ? p_title : "New agent session"); snprintf( p_entity->text, sizeof(p_entity->text), "You\n%s\n\nCopilot\n%s", p_prompt, p_response); p_scene->selected_index = (int32)Dowa_Array_Length(p_scene->p_entities) - 1; return p_entity->id; } boolean Canvas_Scene_Append_Conversation( Canvas_Scene *p_scene, uint32 entity_id, const char *p_prompt, const char *p_response) { for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { Canvas_Entity *p_entity = &p_scene->p_entities[index]; if (p_entity->id != entity_id || p_entity->type != CANVAS_ENTITY_CONVERSATION) { continue; } size_t length = strlen(p_entity->text); snprintf( p_entity->text + length, sizeof(p_entity->text) - length, "\n\nYou\n%s\n\nCopilot\n%s", p_prompt, p_response); p_scene->selected_index = (int32)index; return TRUE; } return FALSE; } static float Canvas_Conversation_Max_Scroll( const Canvas_Entity *p_entity, Font font) { Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY]; int32 line_count = Canvas_Text_Build_Lines( font, p_entity->text, p_entity->size.x - 32.0f, lines, CANVAS_ENTITY_TEXT_CAPACITY); return fmaxf( 0.0f, (float)line_count * 18.0f - (CANVAS_CONVERSATION_EXPANDED_HEIGHT - 82.0f)); } void Canvas_Conversation_Scroll_To_End( Canvas_Entity *p_entity, Font font) { if (!p_entity || p_entity->type != CANVAS_ENTITY_CONVERSATION) return; p_entity->value = (int32)Canvas_Conversation_Max_Scroll(p_entity, font); } void Canvas_Conversation_Set_Collapsed( Canvas_Entity *p_entity, boolean collapsed) { if (!p_entity || p_entity->type != CANVAS_ENTITY_CONVERSATION) return; p_entity->active = collapsed; } size_t Canvas_Scene_Parked_Conversation_Count( const Canvas_Scene *p_scene) { size_t count = 0; for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { const Canvas_Entity *p_entity = &p_scene->p_entities[index]; if (p_entity->type == CANVAS_ENTITY_CONVERSATION && p_entity->parked) { count++; } } return count; } boolean Canvas_Scene_Set_Conversation_Parked( Canvas_Scene *p_scene, uint32 entity_id, boolean parked) { Canvas_Entity *p_entity = NULL; for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { if (p_scene->p_entities[index].id == entity_id && p_scene->p_entities[index].type == CANVAS_ENTITY_CONVERSATION) { p_entity = &p_scene->p_entities[index]; break; } } if (!p_entity || p_entity->parked == parked) return FALSE; if (parked) { size_t slot = Canvas_Scene_Parked_Conversation_Count(p_scene); p_entity->parked_restore_position = p_entity->position; p_entity->position = (Vector2){ CANVAS_PARKING_LOT_X + (float)(slot % 3) * 460.0f, CANVAS_PARKING_LOT_Y + (float)(slot / 3) * 340.0f, }; p_entity->parked = TRUE; p_entity->pinned = FALSE; } else { p_entity->position = p_entity->parked_restore_position; p_entity->parked = FALSE; } return TRUE; } boolean Canvas_Scene_Show_Parking_Lot( Canvas_Scene *p_scene, Canvas_Camera *p_camera) { Rectangle bounds = {0}; boolean found = FALSE; for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { Canvas_Entity *p_entity = &p_scene->p_entities[index]; if (p_entity->type != CANVAS_ENTITY_CONVERSATION || !p_entity->parked) { continue; } Rectangle entity_bounds = Canvas_Entity_Bounds(p_entity); if (!found) { bounds = entity_bounds; found = TRUE; } else { float right = fmaxf( bounds.x + bounds.width, entity_bounds.x + entity_bounds.width); float bottom = fmaxf( bounds.y + bounds.height, entity_bounds.y + entity_bounds.height); bounds.x = fminf(bounds.x, entity_bounds.x); bounds.y = fminf(bounds.y, entity_bounds.y); bounds.width = right - bounds.x; bounds.height = bottom - bounds.y; } } if (!found) return FALSE; Canvas_Camera_Fit_Bounds( p_camera, bounds, 80.0f, 80.0f, 80.0f, 80.0f); return TRUE; } void Canvas_Scene_Clear(Canvas_Scene *p_scene) { Dowa_Array_Clear(p_scene->p_entities); p_scene->next_entity = 0; p_scene->selected_index = -1; p_scene->pressed_index = -1; p_scene->hover_index = -1; p_scene->dragging = FALSE; p_scene->resizing = FALSE; p_scene->selecting_text = FALSE; p_scene->editing_web_url = FALSE; p_scene->web_url_entity_id = 0; p_scene->web_url_draft[0] = '\0'; p_scene->notification_stack_amount = 0.0f; } void Canvas_Scene_Fade_Out_All(Canvas_Scene *p_scene) { for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { p_scene->p_entities[index].removing = TRUE; p_scene->p_entities[index].active = FALSE; } p_scene->selected_index = -1; p_scene->pressed_index = -1; p_scene->hover_index = -1; p_scene->dragging = FALSE; p_scene->resizing = FALSE; p_scene->selecting_text = FALSE; p_scene->editing_web_url = FALSE; } void Canvas_Scene_Fade_Out_Selected(Canvas_Scene *p_scene) { if (p_scene->selected_index < 0 || p_scene->selected_index >= (int32)Dowa_Array_Length(p_scene->p_entities)) { return; } Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index]; p_entity->removing = TRUE; p_entity->active = FALSE; p_scene->selected_index = -1; p_scene->pressed_index = -1; p_scene->hover_index = -1; p_scene->dragging = FALSE; p_scene->resizing = FALSE; p_scene->selecting_text = FALSE; p_scene->editing_web_url = FALSE; } void Canvas_Scene_Add_Demo(Canvas_Scene *p_scene) { Canvas_Scene_Clear(p_scene); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_RECTANGLE, (Vector2){-550.0f, -520.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_CIRCLE, (Vector2){-300.0f, -470.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_LINE, (Vector2){-180.0f, -500.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_TEXT, (Vector2){50.0f, -500.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_BUTTON, (Vector2){280.0f, -510.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_SWITCH, (Vector2){500.0f, -504.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_TEXT_AREA, (Vector2){-550.0f, -350.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_DROPDOWN, (Vector2){-230.0f, -350.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_ACCORDION, (Vector2){30.0f, -350.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_CARD, (Vector2){360.0f, -350.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_CALENDAR, (Vector2){-550.0f, -130.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_TABLE, (Vector2){-230.0f, -130.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_NOTIFICATION, (Vector2){220.0f, -130.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_NOTIFICATION, (Vector2){220.0f, -20.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_NOTIFICATION, (Vector2){220.0f, 90.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_SCROLL_AREA, (Vector2){610.0f, -130.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_IMAGE, (Vector2){-550.0f, 220.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_WEB_CONTENT, (Vector2){-150.0f, 120.0f}); Canvas_Scene_Add(p_scene, CANVAS_ENTITY_CONVERSATION, (Vector2){610.0f, 220.0f}); Canvas_Entity *p_conversation = &p_scene->p_entities[Dowa_Array_Length(p_scene->p_entities) - 1]; snprintf( p_conversation->label, sizeof(p_conversation->label), "Spatial agent sessions"); snprintf( p_conversation->text, sizeof(p_conversation->text), "System\nPress M to dictate and Enter to send, or select the text " "area and press Ctrl/Cmd+Enter to ask Copilot."); } void Canvas_Scene_Add_Browser_Grid(Canvas_Scene *p_scene) { Canvas_Scene_Clear(p_scene); for (int32 row = 0; row < 2; row++) { for (int32 column = 0; column < 5; column++) { Vector2 position = { -614.0f + (float)column * 248.0f, -182.0f + (float)row * 188.0f, }; if (!Canvas_Scene_Add(p_scene, CANVAS_ENTITY_WEB_CONTENT, position)) return; Canvas_Entity *p_entity = &p_scene->p_entities[Dowa_Array_Length(p_scene->p_entities) - 1]; p_entity->size = (Vector2){236.0f, 176.0f}; } } } boolean Canvas_Scene_Show_Lucide_Gallery( Canvas_Scene *p_scene, Canvas_Camera *p_camera) { int32 gallery_index = -1; for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { const Canvas_Entity *p_entity = &p_scene->p_entities[index]; if (p_entity->type == CANVAS_ENTITY_LUCIDE_GALLERY) { gallery_index = (int32)index; break; } } if (gallery_index < 0) { if (!Canvas_Scene_Add( p_scene, CANVAS_ENTITY_LUCIDE_GALLERY, (Vector2){1200.0f, -520.0f})) { return FALSE; } gallery_index = (int32)Dowa_Array_Length(p_scene->p_entities) - 1; } Canvas_Entity *p_gallery = &p_scene->p_entities[gallery_index]; p_gallery->removing = FALSE; p_scene->selected_index = gallery_index; Canvas_Camera_Fit_Bounds( p_camera, (Rectangle){ p_gallery->position.x, p_gallery->position.y, p_gallery->size.x, p_gallery->size.y, }, 320.0f, 56.0f, 56.0f, 56.0f); return TRUE; } static float Canvas_Distance_To_Segment(Vector2 point, Vector2 start, Vector2 end) { Vector2 segment = {end.x - start.x, end.y - start.y}; Vector2 relative = {point.x - start.x, point.y - start.y}; float length_squared = segment.x * segment.x + segment.y * segment.y; if (length_squared <= 0.0001f) { return sqrtf(relative.x * relative.x + relative.y * relative.y); } float projection = (relative.x * segment.x + relative.y * segment.y) / length_squared; projection = Canvas_Clamp(projection, 0.0f, 1.0f); Vector2 closest = { start.x + segment.x * projection, start.y + segment.y * projection, }; float dx = point.x - closest.x; float dy = point.y - closest.y; return sqrtf(dx * dx + dy * dy); } static boolean Canvas_Entity_Contains( const Canvas_Entity *p_entity, Vector2 point, float zoom) { switch (p_entity->type) { case CANVAS_ENTITY_RECTANGLE: return CheckCollisionPointRec( point, (Rectangle){ p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }) ? TRUE : FALSE; case CANVAS_ENTITY_CIRCLE: { float dx = point.x - p_entity->position.x; float dy = point.y - p_entity->position.y; return (dx * dx + dy * dy <= p_entity->size.x * p_entity->size.x) ? TRUE : FALSE; } case CANVAS_ENTITY_LINE: { Vector2 end = { p_entity->position.x + p_entity->size.x, p_entity->position.y + p_entity->size.y, }; return Canvas_Distance_To_Segment(point, p_entity->position, end) <= 10.0f / zoom; } case CANVAS_ENTITY_TEXT: case CANVAS_ENTITY_BUTTON: case CANVAS_ENTITY_TEXT_AREA: case CANVAS_ENTITY_DROPDOWN: case CANVAS_ENTITY_ACCORDION: case CANVAS_ENTITY_CARD: case CANVAS_ENTITY_CALENDAR: case CANVAS_ENTITY_SWITCH: case CANVAS_ENTITY_TABLE: case CANVAS_ENTITY_SCROLL_AREA: case CANVAS_ENTITY_IMAGE: case CANVAS_ENTITY_CONVERSATION: { float height = p_entity->size.y; if (p_entity->type == CANVAS_ENTITY_DROPDOWN && p_entity->active) { height += 8.0f + 3.0f * 40.0f; } return CheckCollisionPointRec( point, (Rectangle){ p_entity->position.x, p_entity->position.y, p_entity->size.x, height, }) ? TRUE : FALSE; } case CANVAS_ENTITY_LUCIDE_GALLERY: return CheckCollisionPointRec( point, (Rectangle){ p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }) ? TRUE : FALSE; case CANVAS_ENTITY_NOTIFICATION: if (p_entity->animation_amount <= 0.01f) return FALSE; return CheckCollisionPointRec( point, (Rectangle){ p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }) ? TRUE : FALSE; case CANVAS_ENTITY_WEB_CONTENT: { float frame_inset = CANVAS_WEB_FRAME_INSET / zoom; float resize_handle = CANVAS_WEB_RESIZE_HANDLE / zoom; Rectangle outer = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Rectangle content = { outer.x + frame_inset, outer.y + frame_inset, outer.width - frame_inset - resize_handle, outer.height - frame_inset - resize_handle, }; return CheckCollisionPointRec(point, outer) && !CheckCollisionPointRec(point, content); } default: return FALSE; } } static boolean Canvas_Web_Resize_Handle_Contains( const Canvas_Entity *p_entity, Vector2 point, float zoom) { if (p_entity->type != CANVAS_ENTITY_WEB_CONTENT && p_entity->type != CANVAS_ENTITY_IMAGE) { return FALSE; } float handle_size = CANVAS_WEB_RESIZE_HANDLE / zoom; return CheckCollisionPointRec( point, (Rectangle){ p_entity->position.x + p_entity->size.x - handle_size, p_entity->position.y + p_entity->size.y - handle_size, handle_size, handle_size, }) ? TRUE : FALSE; } int32 Canvas_Scene_Pick( const Canvas_Scene *p_scene, Vector2 world_pointer, float zoom) { for (int32 index = (int32)Dowa_Array_Length(p_scene->p_entities) - 1; index >= 0; index--) { const Canvas_Entity *p_entity = &p_scene->p_entities[index]; if (p_entity->removing) continue; if (p_entity->type == CANVAS_ENTITY_DROPDOWN && p_entity->active && Canvas_Entity_Contains(p_entity, world_pointer, zoom)) { return index; } } for (int32 index = (int32)Dowa_Array_Length(p_scene->p_entities) - 1; index >= 0; index--) { if (p_scene->p_entities[index].removing) continue; if (Canvas_Entity_Contains(&p_scene->p_entities[index], world_pointer, zoom)) return index; } return -1; } int32 Canvas_Scene_Topmost_Visual( const Canvas_Scene *p_scene, Vector2 world_pointer, float zoom) { for (int32 index = (int32)Dowa_Array_Length(p_scene->p_entities) - 1; index >= 0; index--) { const Canvas_Entity *p_entity = &p_scene->p_entities[index]; if (p_entity->removing) continue; if (p_entity->type == CANVAS_ENTITY_WEB_CONTENT) { if (CheckCollisionPointRec( world_pointer, (Rectangle){ p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, })) { return index; } continue; } if (Canvas_Entity_Contains(p_entity, world_pointer, zoom)) { return index; } } return -1; } static void Canvas_Scene_Activate(Canvas_Scene *p_scene, int32 index, Vector2 world_pointer) { if (index < 0 || index >= (int32)Dowa_Array_Length(p_scene->p_entities)) return; Canvas_Entity *p_entity = &p_scene->p_entities[index]; switch (p_entity->type) { case CANVAS_ENTITY_BUTTON: p_entity->active = !p_entity->active; break; case CANVAS_ENTITY_TEXT_AREA: p_entity->active = TRUE; break; case CANVAS_ENTITY_DROPDOWN: if (world_pointer.y <= p_entity->position.y + p_entity->size.y) { p_entity->active = !p_entity->active; } else if (p_entity->active) { float options_y = p_entity->position.y + p_entity->size.y + 8.0f; int32 option = (int32)((world_pointer.y - options_y) / 40.0f); if (option >= 0 && option < 3) p_entity->value = option; p_entity->active = FALSE; } break; case CANVAS_ENTITY_ACCORDION: p_entity->active = !p_entity->active; break; case CANVAS_ENTITY_CARD: case CANVAS_ENTITY_SWITCH: p_entity->active = !p_entity->active; break; case CANVAS_ENTITY_NOTIFICATION: { float local_x = (world_pointer.x - p_entity->position.x) / p_entity->size.x; if (local_x >= 0.68f) p_entity->active = FALSE; break; } case CANVAS_ENTITY_CALENDAR: { float grid_x = p_entity->position.x + 14.0f; float grid_y = p_entity->position.y + 82.0f; if (world_pointer.x < grid_x || world_pointer.y < grid_y || world_pointer.x >= grid_x + 252.0f || world_pointer.y >= grid_y + 192.0f) { break; } int32 column = (int32)((world_pointer.x - grid_x) / 36.0f); int32 row = (int32)((world_pointer.y - grid_y) / 32.0f); int32 day = row * 7 + column - 6 + 1; if (column >= 0 && column < 7 && row >= 0 && row < 6 && day >= 1 && day <= 31) { p_entity->value = day; } break; } case CANVAS_ENTITY_TABLE: { float rows_y = p_entity->position.y + 62.0f; int32 row = (int32)((world_pointer.y - rows_y) / 38.0f); if (world_pointer.y >= rows_y && row >= 0 && row < 4) { p_entity->value = row; } break; } default: break; } } static boolean Canvas_Shift_Is_Down(void) { return IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); } static boolean Canvas_Alt_Is_Down(void) { return IsKeyDown(KEY_LEFT_ALT) || IsKeyDown(KEY_RIGHT_ALT); } static boolean Canvas_Super_Is_Down(void) { return IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_RIGHT_SUPER); } static boolean Canvas_Key_Activated(int32 key) { return IsKeyPressed(key) || IsKeyPressedRepeat(key); } static float Canvas_Text_Measure_Range( Font font, const char *p_text, int32 start, int32 end) { if (end <= start) return 0.0f; char buffer[CANVAS_ENTITY_TEXT_CAPACITY]; int32 length = end - start; if (length >= (int32)sizeof(buffer)) length = (int32)sizeof(buffer) - 1; memcpy(buffer, p_text + start, (size_t)length); buffer[length] = '\0'; return MeasureTextEx( font, buffer, CANVAS_TEXT_AREA_FONT_SIZE, CANVAS_TEXT_AREA_SPACING).x; } static int32 Canvas_Text_Build_Lines( Font font, const char *p_text, float max_width, Canvas_Text_Line *p_lines, int32 line_capacity) { int32 length = (int32)strlen(p_text); int32 line_count = 0; int32 start = 0; while (start <= length && line_count < line_capacity) { int32 index = start; int32 last_space = -1; float width = 0.0f; boolean ended_with_newline = FALSE; while (index < length) { if (p_text[index] == '\n') { ended_with_newline = TRUE; break; } float character_width = Canvas_Text_Measure_Range( font, p_text, index, index + 1); if (width + character_width > max_width && index > start) { if (last_space >= start) index = last_space + 1; break; } width += character_width; if (p_text[index] == ' ' || p_text[index] == '\t') { last_space = index; } index++; } int32 end = index; int32 next = ended_with_newline ? index + 1 : index; p_lines[line_count++] = (Canvas_Text_Line){ .start = start, .end = end, .next = next, .width = Canvas_Text_Measure_Range(font, p_text, start, end), }; if (index >= length) break; start = next; } return line_count; } static Rectangle Canvas_Text_Area_Content_Bounds( const Canvas_Entity *p_entity) { float header_height = p_entity->label[0] ? 26.0f : 0.0f; return (Rectangle){ p_entity->position.x + CANVAS_TEXT_AREA_PADDING, p_entity->position.y + CANVAS_TEXT_AREA_PADDING + header_height, p_entity->size.x - CANVAS_TEXT_AREA_PADDING * 2.0f, p_entity->size.y - CANVAS_TEXT_AREA_PADDING * 2.0f - header_height, }; } 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); } void Canvas_Text_Area_Set_Content( Canvas_Entity *p_entity, Font font, const char *p_text, boolean muted) { if (!p_entity || p_entity->type != CANVAS_ENTITY_TEXT_AREA) return; snprintf(p_entity->text, sizeof(p_entity->text), "%s", p_text ? p_text : ""); p_entity->text_cursor = (int32)strlen(p_entity->text); p_entity->text_selection_anchor = p_entity->text_cursor; p_entity->text_muted = muted; 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); p_entity->text_scroll_y = fmaxf( 0.0f, (float)line_count * CANVAS_TEXT_AREA_LINE_HEIGHT - content.height); } static void Canvas_Text_Update_Keyboard(Canvas_Entity *p_entity, Font font) { boolean shortcut = Canvas_Control_Is_Down(); boolean control = IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL); boolean super = Canvas_Super_Is_Down(); boolean word_modifier = control || Canvas_Alt_Is_Down(); boolean extend = Canvas_Shift_Is_Down(); int32 length = (int32)strlen(p_entity->text); int32 original_cursor = p_entity->text_cursor; int32 original_length = length; if (shortcut && IsKeyPressed(KEY_A)) { p_entity->text_selection_anchor = 0; p_entity->text_cursor = length; } if (shortcut && IsKeyPressed(KEY_C) && Canvas_Text_Has_Selection(p_entity)) { int32 start = 0; int32 end = 0; char selected[CANVAS_ENTITY_TEXT_CAPACITY]; Canvas_Text_Selection_Bounds(p_entity, &start, &end); memcpy(selected, p_entity->text + start, (size_t)(end - start)); selected[end - start] = '\0'; SetClipboardText(selected); } if (shortcut && IsKeyPressed(KEY_X) && Canvas_Text_Has_Selection(p_entity)) { int32 start = 0; int32 end = 0; char selected[CANVAS_ENTITY_TEXT_CAPACITY]; Canvas_Text_Selection_Bounds(p_entity, &start, &end); memcpy(selected, p_entity->text + start, (size_t)(end - start)); selected[end - start] = '\0'; SetClipboardText(selected); Canvas_Text_Delete_Selection(p_entity); } if (shortcut && IsKeyPressed(KEY_V)) { const char *p_clipboard = GetClipboardText(); if (p_clipboard) Canvas_Text_Insert(p_entity, p_clipboard); } if (Canvas_Key_Activated(KEY_LEFT)) { int32 cursor = p_entity->text_cursor; if (!extend && Canvas_Text_Has_Selection(p_entity)) { int32 end = 0; Canvas_Text_Selection_Bounds(p_entity, &cursor, &end); } else if (super) { Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY]; Rectangle content = Canvas_Text_Area_Content_Bounds(p_entity); int32 count = Canvas_Text_Build_Lines( font, p_entity->text, content.width, lines, CANVAS_ENTITY_TEXT_CAPACITY); cursor = lines[Canvas_Text_Line_For_Cursor( lines, count, cursor)].start; } else { cursor = word_modifier ? Canvas_Text_Previous_Word(p_entity->text, cursor) : cursor - 1; } Canvas_Text_Move_Cursor(p_entity, cursor, extend); } if (Canvas_Key_Activated(KEY_RIGHT)) { int32 cursor = p_entity->text_cursor; if (!extend && Canvas_Text_Has_Selection(p_entity)) { int32 start = 0; Canvas_Text_Selection_Bounds(p_entity, &start, &cursor); } else if (super) { Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY]; Rectangle content = Canvas_Text_Area_Content_Bounds(p_entity); int32 count = Canvas_Text_Build_Lines( font, p_entity->text, content.width, lines, CANVAS_ENTITY_TEXT_CAPACITY); cursor = lines[Canvas_Text_Line_For_Cursor( lines, count, cursor)].end; } else { cursor = word_modifier ? Canvas_Text_Next_Word(p_entity->text, cursor) : cursor + 1; } Canvas_Text_Move_Cursor(p_entity, cursor, extend); } Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY]; Rectangle content = Canvas_Text_Area_Content_Bounds(p_entity); int32 line_count = Canvas_Text_Build_Lines( font, p_entity->text, content.width, lines, CANVAS_ENTITY_TEXT_CAPACITY); int32 current_line = Canvas_Text_Line_For_Cursor( lines, line_count, p_entity->text_cursor); float cursor_x = Canvas_Text_Measure_Range( font, p_entity->text, lines[current_line].start, p_entity->text_cursor); if (Canvas_Key_Activated(KEY_UP) || Canvas_Key_Activated(KEY_DOWN)) { int32 target_line = current_line + (IsKeyDown(KEY_UP) ? -1 : 1); if (target_line < 0) target_line = 0; if (target_line >= line_count) target_line = line_count - 1; Vector2 target = { content.x + cursor_x, content.y + (float)target_line * CANVAS_TEXT_AREA_LINE_HEIGHT - p_entity->text_scroll_y + CANVAS_TEXT_AREA_LINE_HEIGHT * 0.5f, }; Canvas_Text_Move_Cursor( p_entity, Canvas_Text_Cursor_At_Point(font, p_entity, target), extend); } if (Canvas_Key_Activated(KEY_HOME)) { Canvas_Text_Move_Cursor( p_entity, lines[current_line].start, extend); } if (Canvas_Key_Activated(KEY_END)) { Canvas_Text_Move_Cursor( p_entity, lines[current_line].end, extend); } if (Canvas_Key_Activated(KEY_BACKSPACE)) { if (Canvas_Text_Has_Selection(p_entity)) { Canvas_Text_Delete_Selection(p_entity); } else if (p_entity->text_cursor > 0) { int32 start = p_entity->text_cursor - 1; if (super) { start = lines[Canvas_Text_Line_For_Cursor( lines, line_count, p_entity->text_cursor)].start; } else if (word_modifier) { start = Canvas_Text_Previous_Word( p_entity->text, p_entity->text_cursor); } p_entity->text_selection_anchor = start; Canvas_Text_Delete_Selection(p_entity); } } if (Canvas_Key_Activated(KEY_DELETE)) { if (Canvas_Text_Has_Selection(p_entity)) { Canvas_Text_Delete_Selection(p_entity); } else if (p_entity->text_cursor < length) { int32 end = p_entity->text_cursor + 1; if (super) { end = lines[Canvas_Text_Line_For_Cursor( lines, line_count, p_entity->text_cursor)].end; } else if (word_modifier) { end = Canvas_Text_Next_Word( p_entity->text, p_entity->text_cursor); } p_entity->text_selection_anchor = end; Canvas_Text_Delete_Selection(p_entity); } } if (IsKeyPressed(KEY_ENTER)) Canvas_Text_Insert(p_entity, "\n"); int32 codepoint = GetCharPressed(); while (codepoint > 0) { if (!shortcut && codepoint >= 32 && codepoint <= 126) { char inserted[2] = {(char)codepoint, '\0'}; Canvas_Text_Insert(p_entity, inserted); } codepoint = GetCharPressed(); } if (p_entity->text_cursor != original_cursor || (int32)strlen(p_entity->text) != original_length) { Canvas_Text_Ensure_Cursor_Visible(p_entity, font); } } static void Canvas_Scene_Update_Text_Input( Canvas_Scene *p_scene, Font font) { if (p_scene->selected_index < 0 || p_scene->selected_index >= (int32)Dowa_Array_Length(p_scene->p_entities)) return; Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index]; if ((p_entity->type != CANVAS_ENTITY_TEXT_AREA && p_entity->type != CANVAS_ENTITY_LUCIDE_GALLERY) || !p_entity->active) { return; } if (p_entity->type == CANVAS_ENTITY_TEXT_AREA) { Canvas_Text_Update_Keyboard(p_entity, font); return; } size_t length = strlen(p_entity->text); boolean changed = FALSE; int32 codepoint = GetCharPressed(); while (codepoint > 0) { size_t input_capacity = p_entity->type == CANVAS_ENTITY_LUCIDE_GALLERY ? 64 : sizeof(p_entity->text); if (codepoint >= 32 && codepoint <= 126 && length + 1 < input_capacity) { p_entity->text[length++] = p_entity->type == CANVAS_ENTITY_LUCIDE_GALLERY ? Canvas_Ascii_Lower((char)codepoint) : (char)codepoint; p_entity->text[length] = '\0'; changed = TRUE; } codepoint = GetCharPressed(); } if (p_entity->type == CANVAS_ENTITY_TEXT_AREA && IsKeyPressed(KEY_ENTER) && length + 1 < sizeof(p_entity->text)) { p_entity->text[length++] = '\n'; p_entity->text[length] = '\0'; } if (IsKeyPressed(KEY_BACKSPACE) && length > 0) { p_entity->text[length - 1] = '\0'; changed = TRUE; } if (changed && p_entity->type == CANVAS_ENTITY_LUCIDE_GALLERY) { p_entity->value = 0; } } static void Canvas_Entity_Capture_Pin( Canvas_Entity *p_entity, const Canvas_Camera *p_camera) { p_entity->pinned_screen_position = Canvas_Camera_World_To_Screen(p_camera, p_entity->position); p_entity->pinned_screen_size = (Vector2){ p_entity->size.x * p_camera->zoom, p_entity->size.y * p_camera->zoom, }; } static void Canvas_Scene_Adjust_Index_After_Removal( int32 *p_index, int32 removed_index) { if (*p_index == removed_index) { *p_index = -1; } else if (*p_index > removed_index) { (*p_index)--; } } static void Canvas_Scene_Update_Lifecycle(Canvas_Scene *p_scene) { float follow = 1.0f - expf(-GetFrameTime() * 9.0f); size_t index = 0; while (index < Dowa_Array_Length(p_scene->p_entities)) { Canvas_Entity *p_entity = &p_scene->p_entities[index]; float target = p_entity->removing ? 0.0f : 1.0f; p_entity->visibility_amount += (target - p_entity->visibility_amount) * follow; p_entity->visibility_amount = Canvas_Clamp( p_entity->visibility_amount, 0.0f, 1.0f); float chrome_target = p_entity->type == CANVAS_ENTITY_WEB_CONTENT && (int32)index == p_scene->selected_index && !p_entity->removing ? 1.0f : 0.0f; float chrome_follow = 1.0f - expf(-GetFrameTime() * 12.0f); p_entity->web_chrome_amount += (chrome_target - p_entity->web_chrome_amount) * chrome_follow; p_entity->web_chrome_amount = Canvas_Clamp( p_entity->web_chrome_amount, 0.0f, 1.0f); if (p_entity->removing && p_entity->visibility_amount <= 0.01f) { int32 removed_index = (int32)index; size_t length = Dowa_Array_Length(p_scene->p_entities); for (size_t move = index + 1; move < length; move++) { p_scene->p_entities[move - 1] = p_scene->p_entities[move]; } (void)Dowa_Array_Pop(p_scene->p_entities); Canvas_Scene_Adjust_Index_After_Removal( &p_scene->selected_index, removed_index); Canvas_Scene_Adjust_Index_After_Removal( &p_scene->pressed_index, removed_index); Canvas_Scene_Adjust_Index_After_Removal( &p_scene->hover_index, removed_index); continue; } index++; } } static void Canvas_Scene_Update_Animations( Canvas_Scene *p_scene, const Canvas_Camera *p_camera) { for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { Canvas_Entity *p_entity = &p_scene->p_entities[index]; if (p_entity->type != CANVAS_ENTITY_ACCORDION && p_entity->type != CANVAS_ENTITY_SWITCH && p_entity->type != CANVAS_ENTITY_CONVERSATION) { continue; } float target = p_entity->type == CANVAS_ENTITY_CONVERSATION ? (p_entity->active ? 0.0f : 1.0f) : (p_entity->active ? 1.0f : 0.0f); float speed = p_entity->type == CANVAS_ENTITY_SWITCH ? 7.0f : 4.5f; float step = GetFrameTime() * speed; if (p_entity->animation_amount < target) { p_entity->animation_amount = fminf( target, p_entity->animation_amount + step); } else if (p_entity->animation_amount > target) { p_entity->animation_amount = fmaxf( target, p_entity->animation_amount - step); } float amount = p_entity->animation_amount; float eased = amount * amount * (3.0f - 2.0f * amount); if (p_entity->type == CANVAS_ENTITY_SWITCH) continue; if (p_entity->type == CANVAS_ENTITY_CONVERSATION) { float height = CANVAS_CONVERSATION_COLLAPSED_HEIGHT + (CANVAS_CONVERSATION_EXPANDED_HEIGHT - CANVAS_CONVERSATION_COLLAPSED_HEIGHT) * eased; if (p_entity->pinned) { float pinned_scale = p_entity->pinned_screen_size.x / p_entity->size.x; p_entity->pinned_screen_size.y = height * pinned_scale; p_entity->size.y = p_entity->pinned_screen_size.y / p_camera->zoom; } else { p_entity->size.y = height; } continue; } if (p_entity->pinned) { float pinned_scale = p_entity->pinned_screen_size.x / 300.0f; p_entity->pinned_screen_size.y = (56.0f + 108.0f * eased) * pinned_scale; p_entity->size.y = p_entity->pinned_screen_size.y / p_camera->zoom; } else { p_entity->size.y = 56.0f + 108.0f * eased; } } } boolean Canvas_Scene_Update_Interaction( Canvas_Scene *p_scene, const Canvas_Camera *p_camera, Font font, boolean block_pointer_input) { Canvas_Scene_Update_Lifecycle(p_scene); Canvas_Scene_Update_Text_Input(p_scene, font); Canvas_Scene_Update_Animations(p_scene, p_camera); if (block_pointer_input && p_scene->pressed_index < 0) { return FALSE; } Vector2 world_pointer = Canvas_Camera_Screen_To_World(p_camera, GetMousePosition()); p_scene->hover_index = block_pointer_input ? -1 : Canvas_Scene_Pick(p_scene, world_pointer, p_camera->zoom); boolean consumed_scroll = FALSE; if (p_scene->hover_index >= 0) { Canvas_Entity *p_hovered = &p_scene->p_entities[p_scene->hover_index]; boolean control_down = Canvas_Control_Is_Down(); if (p_hovered->type == CANVAS_ENTITY_TEXT_AREA && p_hovered->active && !control_down) { float wheel = GetMouseWheelMove(); if (wheel != 0.0f) { Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY]; Rectangle content = Canvas_Text_Area_Content_Bounds(p_hovered); int32 line_count = Canvas_Text_Build_Lines( font, p_hovered->text, content.width, lines, CANVAS_ENTITY_TEXT_CAPACITY); float max_scroll = fmaxf( 0.0f, (float)line_count * CANVAS_TEXT_AREA_LINE_HEIGHT - content.height); p_hovered->text_scroll_y = Canvas_Clamp( p_hovered->text_scroll_y - wheel * CANVAS_TEXT_AREA_LINE_HEIGHT * 2.0f, 0.0f, max_scroll); consumed_scroll = TRUE; } } else if (p_hovered->type == CANVAS_ENTITY_SCROLL_AREA && !control_down) { float wheel = GetMouseWheelMove(); if (wheel != 0.0f) { p_hovered->value = (int32)Canvas_Clamp( (float)p_hovered->value - wheel * 28.0f, 0.0f, 240.0f); consumed_scroll = TRUE; } } else if (p_hovered->type == CANVAS_ENTITY_CONVERSATION && !control_down) { float wheel = GetMouseWheelMove(); if (wheel != 0.0f) { Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY]; int32 line_count = Canvas_Text_Build_Lines( font, p_hovered->text, p_hovered->size.x - 32.0f, lines, CANVAS_ENTITY_TEXT_CAPACITY); float max_scroll = fmaxf( 0.0f, (float)line_count * 18.0f - (p_hovered->size.y - 82.0f)); p_hovered->value = (int32)Canvas_Clamp( (float)p_hovered->value - wheel * 36.0f, 0.0f, max_scroll); consumed_scroll = TRUE; } } else if (p_hovered->type == CANVAS_ENTITY_LUCIDE_GALLERY && !control_down) { float wheel = GetMouseWheelMove(); if (wheel != 0.0f) { p_hovered->value = (int32)Canvas_Clamp( (float)p_hovered->value - wheel * 72.0f, 0.0f, Canvas_Lucide_Max_Scroll(p_hovered)); consumed_scroll = TRUE; } } } float animation_step = Canvas_Clamp(GetFrameTime() * 12.0f, 0.0f, 1.0f); for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { float target = ((int32)index == p_scene->hover_index) ? 1.0f : 0.0f; Canvas_Entity *p_entity = &p_scene->p_entities[index]; p_entity->hover_amount += (target - p_entity->hover_amount) * animation_step; } if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && !IsKeyDown(KEY_SPACE)) { p_scene->selected_index = Canvas_Scene_Pick(p_scene, world_pointer, p_camera->zoom); for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { if ((int32)index != p_scene->selected_index && p_scene->p_entities[index].type == CANVAS_ENTITY_TEXT_AREA) { p_scene->p_entities[index].active = FALSE; } if ((int32)index != p_scene->selected_index && p_scene->p_entities[index].type == CANVAS_ENTITY_DROPDOWN) { p_scene->p_entities[index].active = FALSE; } if ((int32)index != p_scene->selected_index && p_scene->p_entities[index].type == CANVAS_ENTITY_LUCIDE_GALLERY) { p_scene->p_entities[index].active = FALSE; } } if (p_scene->selected_index >= 0) { Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index]; if (p_entity->type == CANVAS_ENTITY_CONVERSATION && CheckCollisionPointRec( world_pointer, Canvas_Conversation_Park_Bounds(p_entity))) { Canvas_Scene_Set_Conversation_Parked( p_scene, p_entity->id, p_entity->parked ? FALSE : TRUE); p_scene->pressed_index = -1; return TRUE; } if (p_entity->type == CANVAS_ENTITY_CONVERSATION && CheckCollisionPointRec( world_pointer, Canvas_Conversation_Collapse_Bounds(p_entity))) { Canvas_Conversation_Set_Collapsed( p_entity, p_entity->active ? FALSE : TRUE); p_scene->pressed_index = -1; return TRUE; } if (p_entity->type == CANVAS_ENTITY_LUCIDE_GALLERY) { p_entity->active = CheckCollisionPointRec( world_pointer, Canvas_Lucide_Search_Bounds(p_entity)) ? TRUE : FALSE; } p_scene->selecting_text = FALSE; if (p_entity->type == CANVAS_ENTITY_TEXT_AREA) { boolean inside_content = CheckCollisionPointRec( world_pointer, Canvas_Text_Area_Content_Bounds(p_entity)) ? TRUE : FALSE; p_entity->active = inside_content; if (inside_content) { int32 cursor = Canvas_Text_Cursor_At_Point( font, p_entity, world_pointer); p_entity->text_cursor = cursor; if (!Canvas_Shift_Is_Down()) { p_entity->text_selection_anchor = cursor; } p_scene->selecting_text = TRUE; } } p_scene->pressed_index = p_scene->selected_index; p_scene->dragging = FALSE; p_scene->resizing = Canvas_Web_Resize_Handle_Contains( p_entity, world_pointer, p_camera->zoom); p_scene->pointer_start = world_pointer; p_scene->entity_start = p_entity->position; p_scene->entity_size_start = p_entity->size; } } if (p_scene->pressed_index >= 0 && IsMouseButtonDown(MOUSE_BUTTON_LEFT)) { Vector2 delta = { world_pointer.x - p_scene->pointer_start.x, world_pointer.y - p_scene->pointer_start.y, }; float screen_distance_squared = (delta.x * p_camera->zoom) * (delta.x * p_camera->zoom) + (delta.y * p_camera->zoom) * (delta.y * p_camera->zoom); Canvas_Entity *p_pressed = &p_scene->p_entities[p_scene->pressed_index]; if (p_scene->selecting_text && p_pressed->type == CANVAS_ENTITY_TEXT_AREA) { p_pressed->text_cursor = Canvas_Text_Cursor_At_Point( font, p_pressed, world_pointer); Canvas_Text_Ensure_Cursor_Visible(p_pressed, font); } if (screen_distance_squared >= 16.0f && !p_scene->selecting_text && !(p_pressed->type == CANVAS_ENTITY_LUCIDE_GALLERY && p_pressed->active)) { p_scene->dragging = TRUE; } if (p_scene->dragging) { Canvas_Entity *p_entity = p_pressed; if (p_entity->type == CANVAS_ENTITY_NOTIFICATION) { p_scene->dragging = FALSE; } else if (p_scene->resizing) { p_entity->size = (Vector2){ fmaxf(CANVAS_WEB_MIN_WIDTH, p_scene->entity_size_start.x + delta.x), fmaxf(CANVAS_WEB_MIN_HEIGHT, p_scene->entity_size_start.y + delta.y), }; } else { p_entity->position = (Vector2){ p_scene->entity_start.x + delta.x, p_scene->entity_start.y + delta.y, }; } if (p_entity->pinned) Canvas_Entity_Capture_Pin(p_entity, p_camera); } } if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) { if (p_scene->pressed_index >= 0 && !p_scene->dragging) { Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->pressed_index]; if (p_entity->type != CANVAS_ENTITY_TEXT_AREA) { Canvas_Scene_Activate( p_scene, p_scene->pressed_index, world_pointer); } if (p_entity->pinned) Canvas_Entity_Capture_Pin(p_entity, p_camera); } p_scene->pressed_index = -1; p_scene->dragging = FALSE; p_scene->resizing = FALSE; p_scene->selecting_text = FALSE; } return p_scene->pressed_index >= 0 || consumed_scroll; } boolean Canvas_Scene_Is_Text_Editing(const Canvas_Scene *p_scene) { if (p_scene->selected_index < 0 || p_scene->selected_index >= (int32)Dowa_Array_Length(p_scene->p_entities)) { return FALSE; } const Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index]; return ((p_entity->type == CANVAS_ENTITY_TEXT_AREA || p_entity->type == CANVAS_ENTITY_LUCIDE_GALLERY) && p_entity->active) ? TRUE : FALSE; } void Canvas_Scene_Toggle_Selected_Pin( Canvas_Scene *p_scene, const Canvas_Camera *p_camera) { if (p_scene->selected_index < 0 || p_scene->selected_index >= (int32)Dowa_Array_Length(p_scene->p_entities)) { return; } Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index]; if (p_entity->type == CANVAS_ENTITY_NOTIFICATION) return; p_entity->pinned = !p_entity->pinned; if (p_entity->pinned) Canvas_Entity_Capture_Pin(p_entity, p_camera); } void Canvas_Scene_Update_Notification_Stack( Canvas_Scene *p_scene, const Canvas_Camera *p_camera) { const float width = 384.0f; const float height = 82.0f; const float margin = 24.0f; const float gap = 12.0f; float delta = GetFrameTime(); float follow = 1.0f - expf(-delta * 16.0f); Vector2 mouse = GetMousePosition(); boolean hovered = FALSE; int32 active_count = 0; for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { Canvas_Entity *p_entity = &p_scene->p_entities[index]; if (p_entity->type != CANVAS_ENTITY_NOTIFICATION) continue; float visibility_target = p_entity->active ? 1.0f : 0.0f; if (p_entity->animation_amount < visibility_target) { p_entity->animation_amount = fminf( visibility_target, p_entity->animation_amount + delta * 7.0f); } else if (p_entity->animation_amount > visibility_target) { p_entity->animation_amount = fmaxf( visibility_target, p_entity->animation_amount - delta * 7.0f); } if (p_entity->active) active_count++; if (p_entity->screen_space_initialized && p_entity->animation_amount > 0.01f && CheckCollisionPointRec( mouse, (Rectangle){ p_entity->pinned_screen_position.x, p_entity->pinned_screen_position.y, p_entity->pinned_screen_size.x, p_entity->pinned_screen_size.y, })) { hovered = TRUE; } } if (!hovered && active_count > 1 && p_scene->notification_stack_amount > 0.08f) { float right = (float)p_camera->viewport_width - margin; float bottom = (float)p_camera->viewport_height - margin; float top = bottom - (float)active_count * height - (float)(active_count - 1) * gap; hovered = CheckCollisionPointRec( mouse, (Rectangle){right - width, top, width, bottom - top}) ? TRUE : FALSE; } float stack_target = hovered ? 1.0f : 0.0f; p_scene->notification_stack_amount += (stack_target - p_scene->notification_stack_amount) * follow; float amount = p_scene->notification_stack_amount; float eased = amount * amount * (3.0f - 2.0f * amount); int32 rank = 0; for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { Canvas_Entity *p_entity = &p_scene->p_entities[index]; if (p_entity->type != CANVAS_ENTITY_NOTIFICATION || p_entity->animation_amount <= 0.01f) { continue; } Vector2 target_position = p_entity->pinned_screen_position; Vector2 target_size = {width, height}; if (p_entity->active) { int32 depth = active_count - rank - 1; Vector2 collapsed_position = { (float)p_camera->viewport_width - margin - width + (float)depth * 8.0f, (float)p_camera->viewport_height - margin - height - (float)depth * 8.0f, }; Vector2 expanded_position = { (float)p_camera->viewport_width - margin - width, (float)p_camera->viewport_height - margin - height - (float)depth * (height + gap), }; target_position = (Vector2){ collapsed_position.x + (expanded_position.x - collapsed_position.x) * eased, collapsed_position.y + (expanded_position.y - collapsed_position.y) * eased, }; target_size.x = width - (float)depth * 12.0f * (1.0f - eased); rank++; } else { target_position.x = (float)p_camera->viewport_width + 20.0f; } if (!p_entity->screen_space_initialized) { p_entity->pinned_screen_position = target_position; p_entity->pinned_screen_size = target_size; p_entity->screen_space_initialized = TRUE; } else { p_entity->pinned_screen_position.x += (target_position.x - p_entity->pinned_screen_position.x) * follow; p_entity->pinned_screen_position.y += (target_position.y - p_entity->pinned_screen_position.y) * follow; p_entity->pinned_screen_size.x += (target_size.x - p_entity->pinned_screen_size.x) * follow; p_entity->pinned_screen_size.y += (target_size.y - p_entity->pinned_screen_size.y) * follow; } p_entity->position = Canvas_Camera_Screen_To_World( p_camera, p_entity->pinned_screen_position); p_entity->size = (Vector2){ p_entity->pinned_screen_size.x / p_camera->zoom, p_entity->pinned_screen_size.y / p_camera->zoom, }; } } void Canvas_Scene_Sync_Pinned( Canvas_Scene *p_scene, const Canvas_Camera *p_camera) { for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { Canvas_Entity *p_entity = &p_scene->p_entities[index]; if (!p_entity->pinned) continue; Vector2 min_offset = {0.0f, 0.0f}; Vector2 max_offset = p_entity->pinned_screen_size; if (p_entity->type == CANVAS_ENTITY_CIRCLE) { min_offset = (Vector2){ -p_entity->pinned_screen_size.x, -p_entity->pinned_screen_size.x, }; max_offset = (Vector2){ p_entity->pinned_screen_size.x, p_entity->pinned_screen_size.x, }; } else if (p_entity->type == CANVAS_ENTITY_LINE) { min_offset = (Vector2){ fminf(0.0f, p_entity->pinned_screen_size.x), fminf(0.0f, p_entity->pinned_screen_size.y), }; max_offset = (Vector2){ fmaxf(0.0f, p_entity->pinned_screen_size.x), fmaxf(0.0f, p_entity->pinned_screen_size.y), }; } float min_x = -min_offset.x; float min_y = -min_offset.y; float max_x = fmaxf( min_x, (float)p_camera->viewport_width - max_offset.x); float max_y = fmaxf( min_y, (float)p_camera->viewport_height - max_offset.y); p_entity->pinned_screen_position.x = Canvas_Clamp( p_entity->pinned_screen_position.x, min_x, max_x); p_entity->pinned_screen_position.y = Canvas_Clamp( p_entity->pinned_screen_position.y, min_y, max_y); p_entity->position = Canvas_Camera_Screen_To_World( p_camera, p_entity->pinned_screen_position); p_entity->size = (Vector2){ p_entity->pinned_screen_size.x / p_camera->zoom, p_entity->pinned_screen_size.y / p_camera->zoom, }; } } void Canvas_Update_Camera_Input( Canvas_Camera *p_camera, boolean block_pointer_input, boolean block_keyboard_input) { boolean keyboard_pan = IsKeyDown(KEY_SPACE); boolean pointer_pan = IsMouseButtonDown(MOUSE_BUTTON_MIDDLE) || (keyboard_pan && IsMouseButtonDown(MOUSE_BUTTON_LEFT)); if (pointer_pan && !block_pointer_input) { Canvas_Camera_Pan(p_camera, GetMouseDelta()); } if (!block_pointer_input) { float wheel = GetMouseWheelMove(); if (wheel != 0.0f) { if (Canvas_Control_Is_Down()) { Canvas_Camera_Zoom_At( p_camera, GetMousePosition(), powf(1.15f, wheel)); } else { Canvas_Camera_Pan( p_camera, (Vector2){0.0f, wheel * 72.0f}); } } } if (!block_keyboard_input) { Vector2 keyboard_delta = {0.0f, 0.0f}; float speed = 600.0f * GetFrameTime(); if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT)) keyboard_delta.x += speed; if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT)) keyboard_delta.x -= speed; if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP)) keyboard_delta.y += speed; if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN)) keyboard_delta.y -= speed; Canvas_Camera_Pan(p_camera, keyboard_delta); } } static void Canvas_Draw_Grid( const Canvas_Camera *p_camera, const Canvas_Theme *p_theme) { Rectangle bounds = Canvas_Camera_Viewport_Bounds(p_camera); float spacing = Canvas_Grid_Spacing(p_camera->zoom); int32 first_x = (int32)floorf(bounds.x / spacing); int32 last_x = (int32)ceilf((bounds.x + bounds.width) / spacing); int32 first_y = (int32)floorf(bounds.y / spacing); int32 last_y = (int32)ceilf((bounds.y + bounds.height) / spacing); Color minor = Canvas_Color_Fade(p_theme->border, 0.36f); Color major = Canvas_Color_Fade(p_theme->border, 0.65f); Color axis = p_theme->text_muted; for (int32 index = first_x; index <= last_x; index++) { float x = (float)index * spacing; Color color = (index == 0) ? axis : ((index % 4 == 0) ? major : minor); DrawLineV( (Vector2){x, bounds.y}, (Vector2){x, bounds.y + bounds.height}, color); } for (int32 index = first_y; index <= last_y; index++) { float y = (float)index * spacing; Color color = (index == 0) ? axis : ((index % 4 == 0) ? major : minor); DrawLineV( (Vector2){bounds.x, y}, (Vector2){bounds.x + bounds.width, y}, color); } } static void Canvas_Draw_Selection( const Canvas_Entity *p_entity, float zoom, Color selection) { float line = 2.0f / zoom; float padding = 7.0f / zoom; switch (p_entity->type) { case CANVAS_ENTITY_RECTANGLE: DrawRectangleRoundedLinesEx( (Rectangle){ p_entity->position.x - padding, p_entity->position.y - padding, p_entity->size.x + padding * 2.0f, p_entity->size.y + padding * 2.0f, }, 0.14f, 12, line, selection); break; case CANVAS_ENTITY_CIRCLE: DrawCircleLinesV( p_entity->position, p_entity->size.x + padding, selection); break; case CANVAS_ENTITY_LINE: DrawLineEx( p_entity->position, (Vector2){ p_entity->position.x + p_entity->size.x, p_entity->position.y + p_entity->size.y, }, 10.0f / zoom, Canvas_Color_Fade(selection, 0.35f)); break; case CANVAS_ENTITY_TEXT: case CANVAS_ENTITY_BUTTON: case CANVAS_ENTITY_TEXT_AREA: case CANVAS_ENTITY_DROPDOWN: case CANVAS_ENTITY_ACCORDION: case CANVAS_ENTITY_CARD: case CANVAS_ENTITY_CALENDAR: case CANVAS_ENTITY_SWITCH: case CANVAS_ENTITY_TABLE: case CANVAS_ENTITY_NOTIFICATION: case CANVAS_ENTITY_SCROLL_AREA: case CANVAS_ENTITY_IMAGE: case CANVAS_ENTITY_CONVERSATION: { DrawRectangleRoundedLinesEx( (Rectangle){ p_entity->position.x - padding, p_entity->position.y - padding, p_entity->size.x + padding * 2.0f, p_entity->size.y + padding * 2.0f, }, 0.18f, 10, line, selection); break; } case CANVAS_ENTITY_LUCIDE_GALLERY: { DrawRectangleRoundedLinesEx( (Rectangle){ p_entity->position.x - padding, p_entity->position.y - padding, p_entity->size.x + padding * 2.0f, p_entity->size.y + padding * 2.0f, }, 0.04f, 10, line, selection); break; } case CANVAS_ENTITY_WEB_CONTENT: DrawRectangleRoundedLinesEx( (Rectangle){ p_entity->position.x - padding, p_entity->position.y - padding, p_entity->size.x + padding * 2.0f, p_entity->size.y + padding * 2.0f, }, 0.04f, 10, line, selection); DrawRectangleRec( (Rectangle){ p_entity->position.x + p_entity->size.x - CANVAS_WEB_RESIZE_HANDLE / zoom, p_entity->position.y + p_entity->size.y - CANVAS_WEB_RESIZE_HANDLE / zoom, CANVAS_WEB_RESIZE_HANDLE / zoom, CANVAS_WEB_RESIZE_HANDLE / zoom, }, Canvas_Color_Fade(selection, 0.18f)); DrawLineEx( (Vector2){ p_entity->position.x + p_entity->size.x - 11.0f / zoom, p_entity->position.y + p_entity->size.y - 4.0f / zoom, }, (Vector2){ p_entity->position.x + p_entity->size.x - 4.0f / zoom, p_entity->position.y + p_entity->size.y - 11.0f / zoom, }, 1.5f / zoom, selection); break; default: break; } } static void Canvas_Draw_Text_Area_Content( const Canvas_Entity *p_entity, Font font, const Canvas_Theme *p_theme) { Rectangle content = Canvas_Text_Area_Content_Bounds(p_entity); if (!p_entity->text[0]) { boolean dictation = strcmp(p_entity->label, "Dictation") == 0; DrawTextEx( font, dictation ? "Speak now..." : "Type a thought for Copilot...", (Vector2){content.x, content.y}, CANVAS_TEXT_AREA_FONT_SIZE, CANVAS_TEXT_AREA_SPACING, p_theme->text_muted); DrawTextEx( font, dictation ? "Enter to send | Backspace to clear" : "Ctrl/Cmd+Enter to send", (Vector2){content.x, content.y + 22.0f}, 12.0f, 0.0f, Canvas_Color_Fade(p_theme->text_muted, 0.72f)); 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_entity->text_muted ? p_theme->text_muted : p_theme->text); if (p_entity->active && line == cursor_line && fmod(GetTime(), 1.0) < 0.55) { float cursor_x = content.x + Canvas_Text_Measure_Range( font, p_entity->text, lines[line].start, p_entity->text_cursor); DrawLineEx( (Vector2){cursor_x, y}, (Vector2){ cursor_x, y + CANVAS_TEXT_AREA_FONT_SIZE + 2.0f, }, 1.5f, p_theme->accent); } } } static void Canvas_Draw_Entity( const Canvas_Entity *p_entity, Font font, float zoom, boolean selected, boolean pressed, const Canvas_Theme *p_theme) { float opacity = Canvas_Entity_Visibility(p_entity); if (opacity <= 0.001f) return; Canvas_Entity faded_entity = *p_entity; faded_entity.color = Canvas_Color_With_Opacity( faded_entity.color, opacity); Canvas_Theme faded_theme = Canvas_Theme_With_Opacity(p_theme, opacity); p_entity = &faded_entity; p_theme = &faded_theme; float shadow_offset = 6.0f / zoom; Color shadow = p_theme->shadow; static const char *calendar_weekdays[] = { "S", "M", "T", "W", "T", "F", "S", }; switch (p_entity->type) { case CANVAS_ENTITY_RECTANGLE: Canvas_Theme_Draw_Shadow( p_theme, (Rectangle){ p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }, 0.14f, 12, zoom, 1.0f); DrawRectangleRounded( (Rectangle){ p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }, 0.14f, 12, Canvas_Color_Fade(p_entity->color, 0.92f)); DrawRectangleRoundedLinesEx( (Rectangle){ p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }, 0.14f, 12, 1.0f / zoom, p_theme->entity_outline); break; case CANVAS_ENTITY_CIRCLE: DrawCircleV( (Vector2){ p_entity->position.x + shadow_offset, p_entity->position.y + shadow_offset, }, p_entity->size.x, shadow); DrawCircleV(p_entity->position, p_entity->size.x, p_entity->color); break; case CANVAS_ENTITY_LINE: DrawLineEx( p_entity->position, (Vector2){ p_entity->position.x + p_entity->size.x, p_entity->position.y + p_entity->size.y, }, 5.0f, p_entity->color); DrawCircleV(p_entity->position, 2.5f, p_entity->color); DrawCircleV( (Vector2){ p_entity->position.x + p_entity->size.x, p_entity->position.y + p_entity->size.y, }, 2.5f, p_entity->color); break; case CANVAS_ENTITY_TEXT: DrawTextEx( font, p_entity->text, p_entity->position, 28.0f, 0.5f, p_theme->text); break; case CANVAS_ENTITY_BUTTON: { float hover = p_entity->hover_amount; float grow = pressed ? -1.0f : hover * 2.0f; float lift = pressed ? 0.0f : hover * 2.0f; Rectangle bounds = { p_entity->position.x - grow, p_entity->position.y - grow - lift, p_entity->size.x + grow * 2.0f, p_entity->size.y + grow * 2.0f, }; Color base_fill = p_entity->active ? p_theme->button_active : p_theme->button; Color hover_fill = p_entity->active ? p_theme->button_active_hover : p_theme->button_hover; Color fill = Canvas_Color_Lerp(base_fill, hover_fill, hover); Canvas_Theme_Draw_Shadow( p_theme, bounds, 0.46f, 16, zoom, 1.0f); DrawRectangleRounded(bounds, 0.46f, 16, fill); Vector2 text_size = MeasureTextEx(font, p_entity->text, 16.0f, 0.1f); DrawTextEx( font, p_entity->text, (Vector2){ bounds.x + (bounds.width - text_size.x) * 0.5f, bounds.y + (bounds.height - text_size.y) * 0.5f, }, 16.0f, 0.1f, p_theme->on_accent); break; } case CANVAS_ENTITY_TEXT_AREA: { Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.10f, 14, zoom, 1.0f); DrawRectangleRounded(bounds, 0.10f, 14, p_theme->surface); DrawRectangleRoundedLinesEx( bounds, 0.10f, 14, (p_entity->active ? 2.0f : 1.0f) / zoom, p_entity->active ? p_theme->accent : p_theme->border); if (p_entity->label[0]) { DrawTextEx( font, p_entity->label, (Vector2){ bounds.x + CANVAS_TEXT_AREA_PADDING, bounds.y + 11.0f, }, 12.0f, 0.0f, p_theme->text_muted); DrawLineEx( (Vector2){ bounds.x + bounds.width - 38.0f, bounds.y + 17.0f, }, (Vector2){ bounds.x + bounds.width - 16.0f, bounds.y + 17.0f, }, 2.0f / zoom, p_theme->border); } Canvas_Draw_Text_Area_Content(p_entity, font, p_theme); break; } case CANVAS_ENTITY_DROPDOWN: { Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.30f, 14, zoom, 1.0f); DrawRectangleRounded(bounds, 0.30f, 14, p_theme->surface); DrawRectangleRoundedLinesEx( bounds, 0.30f, 14, 1.0f / zoom, p_theme->border); DrawTextEx( font, CANVAS_DROPDOWN_OPTIONS[p_entity->value], (Vector2){bounds.x + 16.0f, bounds.y + 14.0f}, 16.0f, 0.1f, p_theme->text); Vector2 chevron = {bounds.x + bounds.width - 22.0f, bounds.y + bounds.height * 0.5f}; DrawTriangle( (Vector2){chevron.x - 5.0f, chevron.y - 2.0f}, (Vector2){chevron.x, chevron.y + 4.0f}, (Vector2){chevron.x + 5.0f, chevron.y - 2.0f}, p_theme->text_muted); break; } case CANVAS_ENTITY_ACCORDION: { Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.10f, 14, zoom, 1.0f); DrawRectangleRounded(bounds, 0.10f, 14, p_theme->surface); DrawRectangleRoundedLinesEx( bounds, 0.10f, 14, 1.0f / zoom, p_theme->border); DrawTextEx( font, p_entity->text, (Vector2){bounds.x + 16.0f, bounds.y + 18.0f}, 16.0f, 0.1f, p_theme->text); float amount = p_entity->animation_amount; float angle = amount * PI * 0.5f; Vector2 chevron = {bounds.x + bounds.width - 22.0f, bounds.y + 28.0f}; Vector2 direction = {cosf(angle), sinf(angle)}; Vector2 normal = {-direction.y, direction.x}; Vector2 tip = { chevron.x + direction.x * 3.0f, chevron.y + direction.y * 3.0f, }; DrawLineEx( tip, (Vector2){ chevron.x - direction.x * 3.0f + normal.x * 4.0f, chevron.y - direction.y * 3.0f + normal.y * 4.0f, }, 1.5f / zoom, p_theme->text_muted); DrawLineEx( tip, (Vector2){ chevron.x - direction.x * 3.0f - normal.x * 4.0f, chevron.y - direction.y * 3.0f - normal.y * 4.0f, }, 1.5f / zoom, p_theme->text_muted); if (amount > 0.0f) { DrawLineEx( (Vector2){bounds.x, bounds.y + 56.0f}, (Vector2){bounds.x + bounds.width, bounds.y + 56.0f}, 1.0f / zoom, Canvas_Color_Fade(p_theme->border, amount)); if (amount > 0.55f) { float body_alpha = Canvas_Clamp( (amount - 0.55f) / 0.45f, 0.0f, 1.0f); DrawTextEx( font, "Drag, zoom, pin, and combine primitives\nacross one continuous workspace.", (Vector2){bounds.x + 16.0f, bounds.y + 76.0f}, 14.0f, 0.1f, Canvas_Color_Fade(p_theme->text_muted, body_alpha)); } } break; } case CANVAS_ENTITY_CARD: { Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.08f, 14, zoom, 1.0f); DrawRectangleRounded(bounds, 0.08f, 14, p_theme->surface); DrawRectangleRounded( (Rectangle){bounds.x + 12.0f, bounds.y + 12.0f, bounds.width - 24.0f, 92.0f}, 0.08f, 12, p_entity->active ? p_theme->accent_soft : Canvas_Color_Fade(p_entity->color, 0.22f)); DrawCircleV( (Vector2){bounds.x + 44.0f, bounds.y + 58.0f}, 18.0f, p_entity->active ? p_theme->accent : p_entity->color); DrawTextEx( font, p_entity->text, (Vector2){bounds.x + 16.0f, bounds.y + 122.0f}, 20.0f, 0.1f, p_theme->text); DrawTextEx( font, "A flexible surface for tools,\ncontent, and live browser views.", (Vector2){bounds.x + 16.0f, bounds.y + 154.0f}, 14.0f, 0.1f, p_theme->text_muted); DrawRectangleRoundedLinesEx( bounds, 0.08f, 14, (p_entity->active ? 2.0f : 1.0f) / zoom, p_entity->active ? p_theme->accent : p_theme->border); break; } case CANVAS_ENTITY_CALENDAR: { Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.08f, 14, zoom, 1.0f); DrawRectangleRounded(bounds, 0.08f, 14, p_theme->surface); DrawRectangleRoundedLinesEx( bounds, 0.08f, 14, 1.0f / zoom, p_theme->border); DrawTextEx( font, "August 2026", (Vector2){bounds.x + 16.0f, bounds.y + 18.0f}, 20.0f, 0.1f, p_theme->text); for (int32 column = 0; column < 7; column++) { float cell_x = bounds.x + 14.0f + (float)column * 36.0f; DrawTextEx( font, calendar_weekdays[column], (Vector2){cell_x + 12.0f, bounds.y + 56.0f}, 12.0f, 0.0f, p_theme->text_muted); } for (int32 day = 1; day <= 31; day++) { int32 cell = day + 6 - 1; int32 row = cell / 7; int32 column = cell % 7; Vector2 center = { bounds.x + 14.0f + (float)column * 36.0f + 18.0f, bounds.y + 82.0f + (float)row * 32.0f + 16.0f, }; if (day == p_entity->value) { DrawCircleV(center, 14.0f, p_theme->accent); } const char *label = TextFormat("%d", day); Vector2 label_size = MeasureTextEx(font, label, 13.0f, 0.0f); DrawTextEx( font, label, (Vector2){center.x - label_size.x * 0.5f, center.y - label_size.y * 0.5f}, 13.0f, 0.0f, day == p_entity->value ? p_theme->on_accent : p_theme->text); } break; } case CANVAS_ENTITY_SWITCH: { float amount = p_entity->animation_amount; float eased = amount * amount * (3.0f - 2.0f * amount); Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Color off_track = Canvas_Color_Lerp( p_theme->switch_track_off, p_theme->switch_track_off_hover, p_entity->hover_amount); Color on_track = Canvas_Color_Lerp( p_theme->switch_track_on, p_theme->switch_track_on_hover, p_entity->hover_amount); Color track = Canvas_Color_Lerp(off_track, on_track, eased); Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.50f, 18, zoom, 1.0f); DrawRectangleRounded(bounds, 0.50f, 18, track); float knob_x = bounds.x + 18.0f + eased * (bounds.width - 36.0f); float knob_radius = pressed ? 12.5f : 14.0f; DrawCircleV( (Vector2){knob_x, bounds.y + bounds.height * 0.5f}, knob_radius, p_theme->switch_thumb); break; } case CANVAS_ENTITY_TABLE: { Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.06f, 12, zoom, 1.0f); DrawRectangleRounded(bounds, 0.06f, 12, p_theme->surface); DrawRectangleRoundedLinesEx( bounds, 0.06f, 12, 1.0f / zoom, p_theme->border); DrawTextEx( font, "Name", (Vector2){bounds.x + 16.0f, bounds.y + 22.0f}, 13.0f, 0.0f, p_theme->text_muted); DrawTextEx( font, "Status", (Vector2){bounds.x + 292.0f, bounds.y + 22.0f}, 13.0f, 0.0f, p_theme->text_muted); DrawLineEx( (Vector2){bounds.x, bounds.y + 52.0f}, (Vector2){bounds.x + bounds.width, bounds.y + 52.0f}, 1.0f / zoom, p_theme->border); for (int32 row = 0; row < 4; row++) { float row_y = bounds.y + 62.0f + (float)row * 38.0f; if (row == p_entity->value) { DrawRectangle( (int32)(bounds.x + 8.0f), (int32)(row_y - 4.0f), (int32)(bounds.width - 16.0f), 34, p_theme->accent_soft); } DrawTextEx( font, CANVAS_TABLE_NAMES[row], (Vector2){bounds.x + 16.0f, row_y + 5.0f}, 14.0f, 0.0f, p_theme->text); DrawTextEx( font, CANVAS_TABLE_STATUSES[row], (Vector2){bounds.x + 292.0f, row_y + 5.0f}, 14.0f, 0.0f, row == p_entity->value ? p_theme->accent : p_theme->text_muted); } break; } case CANVAS_ENTITY_NOTIFICATION: { if (p_entity->animation_amount <= 0.01f) break; float unit = 1.0f / zoom; Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; float opacity = p_entity->animation_amount; Canvas_Theme_Draw_Shadow( p_theme, bounds, 0.20f, 16, zoom, opacity); DrawRectangleRounded( bounds, 0.20f, 16, Canvas_Color_Fade(p_theme->toast_surface, opacity)); DrawRectangleRoundedLinesEx( bounds, 0.20f, 16, 1.0f / zoom, Canvas_Color_Fade(p_theme->toast_border, opacity)); DrawTextEx( font, p_entity->text, (Vector2){bounds.x + 16.0f * unit, bounds.y + 18.0f * unit}, 15.0f * unit, 0.0f, Canvas_Color_Fade(p_theme->text, opacity)); DrawTextEx( font, "Sunday, December 3 at 9:00 AM", (Vector2){bounds.x + 16.0f * unit, bounds.y + 45.0f * unit}, 14.0f * unit, 0.0f, Canvas_Color_Fade(p_theme->text_muted, opacity)); Rectangle undo = { bounds.x + bounds.width - 108.0f * unit, bounds.y + 24.0f * unit, 54.0f * unit, 34.0f * unit, }; DrawRectangleRounded( undo, 0.26f, 10, Canvas_Color_Fade(p_theme->surface_muted, opacity)); DrawRectangleRoundedLinesEx( undo, 0.26f, 10, 1.0f / zoom, Canvas_Color_Fade(p_theme->border, opacity)); DrawTextEx( font, "Undo", (Vector2){undo.x + 12.0f * unit, undo.y + 10.0f * unit}, 13.0f * unit, 0.0f, Canvas_Color_Fade(p_theme->text, opacity)); Vector2 close = { bounds.x + bounds.width - 24.0f * unit, bounds.y + bounds.height * 0.5f, }; DrawLineEx( (Vector2){close.x - 4.0f * unit, close.y - 4.0f * unit}, (Vector2){close.x + 4.0f * unit, close.y + 4.0f * unit}, 1.5f / zoom, Canvas_Color_Fade(p_theme->text_muted, opacity)); DrawLineEx( (Vector2){close.x + 4.0f * unit, close.y - 4.0f * unit}, (Vector2){close.x - 4.0f * unit, close.y + 4.0f * unit}, 1.5f / zoom, Canvas_Color_Fade(p_theme->text_muted, opacity)); break; } case CANVAS_ENTITY_SCROLL_AREA: { Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.08f, 12, zoom, 1.0f); DrawRectangleRounded(bounds, 0.08f, 12, p_theme->surface); DrawRectangleRoundedLinesEx( bounds, 0.08f, 12, 1.0f / zoom, p_theme->border); DrawTextEx( font, "Scrollable area", (Vector2){bounds.x + 16.0f, bounds.y + 16.0f}, 18.0f, 0.0f, p_theme->text); for (int32 item = 0; item < 10; item++) { float item_y = bounds.y + 52.0f + (float)item * 42.0f - (float)p_entity->value; if (item_y < bounds.y + 46.0f || item_y + 34.0f > bounds.y + bounds.height - 12.0f) { continue; } DrawRectangleRounded( (Rectangle){bounds.x + 14.0f, item_y, bounds.width - 38.0f, 34.0f}, 0.16f, 8, item % 2 == 0 ? p_theme->surface_muted : Canvas_Color_Lerp(p_theme->surface, p_theme->surface_muted, 0.5f)); DrawTextEx( font, TextFormat("Item %d", item + 1), (Vector2){bounds.x + 26.0f, item_y + 9.0f}, 13.0f, 0.0f, p_theme->text); } float thumb_y = bounds.y + 52.0f + ((float)p_entity->value / 240.0f) * (bounds.height - 106.0f); DrawRectangleRounded( (Rectangle){bounds.x + bounds.width - 14.0f, thumb_y, 5.0f, 42.0f}, 0.50f, 6, Canvas_Color_Fade(p_theme->text_muted, 0.82f)); break; } case CANVAS_ENTITY_IMAGE: { Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.04f, 12, zoom, 1.0f); DrawRectangleRounded(bounds, 0.04f, 12, p_theme->surface_muted); DrawRectangleRoundedLinesEx( bounds, 0.04f, 12, 1.0f / zoom, p_theme->border); DrawTextEx( font, "Loading image...", (Vector2){bounds.x + 16.0f, bounds.y + 16.0f}, 14.0f, 0.0f, p_theme->text_muted); break; } case CANVAS_ENTITY_WEB_CONTENT: { Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.04f, 14, zoom, 1.0f); DrawRectangleRounded(bounds, 0.04f, 14, p_theme->surface_muted); DrawRectangleRoundedLinesEx( bounds, 0.04f, 14, 1.0f / zoom, p_theme->border); #if !defined(PLATFORM_WEB) && !defined(__EMSCRIPTEN__) Vector2 label_size = MeasureTextEx( font, "Loading browser surface...", 18.0f, 0.1f); DrawTextEx( font, "Loading browser surface...", (Vector2){ bounds.x + (bounds.width - label_size.x) * 0.5f, bounds.y + (bounds.height - label_size.y) * 0.5f, }, 18.0f, 0.1f, p_theme->text_muted); #endif break; } case CANVAS_ENTITY_CONVERSATION: { Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Canvas_Theme_Draw_Shadow( p_theme, bounds, 0.06f, 14, zoom, 1.0f); DrawRectangleRounded(bounds, 0.06f, 14, p_theme->surface); DrawRectangleRoundedLinesEx( bounds, 0.06f, 14, 1.0f / zoom, p_theme->border); DrawRectangleRounded( (Rectangle){bounds.x, bounds.y, bounds.width, 54.0f}, 0.06f, 14, p_theme->surface_muted); Canvas_Lucide_Draw_Icon( Canvas_Lucide_Find_Icon( p_entity->agent_working ? "sparkles" : "messages-square"), (Vector2){bounds.x + 16.0f, bounds.y + 15.0f}, 0.82f, 1.8f, p_entity->agent_working ? p_theme->accent : p_theme->text); DrawTextEx( font, p_entity->label, (Vector2){bounds.x + 48.0f, bounds.y + 12.0f}, 17.0f, 0.0f, p_theme->text); DrawTextEx( font, p_entity->parked ? "Parked session" : (p_entity->agent_working ? "Thinking..." : "Agent session"), (Vector2){bounds.x + 48.0f, bounds.y + 33.0f}, 11.0f, 0.0f, p_theme->text_muted); Rectangle collapse = Canvas_Conversation_Collapse_Bounds(p_entity); Rectangle park = Canvas_Conversation_Park_Bounds(p_entity); Canvas_Lucide_Draw_Icon( Canvas_Lucide_Find_Icon( p_entity->active ? "chevron-down" : "chevron-up"), (Vector2){collapse.x + 5.0f, collapse.y + 5.0f}, 0.68f, 1.6f, p_theme->text_muted); Canvas_Lucide_Draw_Icon( Canvas_Lucide_Find_Icon( p_entity->parked ? "archive-restore" : "archive"), (Vector2){park.x + 5.0f, park.y + 5.0f}, 0.68f, 1.6f, p_theme->text_muted); if (p_entity->size.y <= 82.0f) break; Rectangle content = { bounds.x + 16.0f, bounds.y + 68.0f, bounds.width - 32.0f, bounds.height - 82.0f, }; 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); for (int32 line_index = 0; line_index < line_count; line_index++) { float y = content.y + (float)line_index * 18.0f - (float)p_entity->value; if (y + 18.0f < content.y || y > content.y + content.height) { continue; } char line_text[CANVAS_ENTITY_TEXT_CAPACITY]; int32 length = lines[line_index].end - lines[line_index].start; memcpy( line_text, p_entity->text + lines[line_index].start, (size_t)length); line_text[length] = '\0'; boolean speaker = strcmp(line_text, "You") == 0 || strcmp(line_text, "Copilot") == 0 || strcmp(line_text, "System") == 0; DrawTextEx( font, line_text, (Vector2){content.x, y}, 14.0f, 0.0f, speaker ? p_theme->accent : p_theme->text); } break; } case CANVAS_ENTITY_LUCIDE_GALLERY: { Rectangle bounds = { p_entity->position.x, p_entity->position.y, p_entity->size.x, p_entity->size.y, }; Canvas_Theme_Draw_Shadow(p_theme, bounds, 0.04f, 14, zoom, 1.0f); DrawRectangleRounded(bounds, 0.04f, 14, p_theme->surface); DrawRectangleRoundedLinesEx( bounds, 0.04f, 14, 1.0f / zoom, p_theme->border); DrawTextEx( font, "Lucide icons", (Vector2){bounds.x + 24.0f, bounds.y + 22.0f}, 26.0f, 0.0f, p_theme->text); DrawTextEx( font, TextFormat( "%d of %d icons - Raylib vectors", Canvas_Lucide_Count_Matches(p_entity->text), CANVAS_LUCIDE_ICON_COUNT), (Vector2){bounds.x + 24.0f, bounds.y + 56.0f}, 13.0f, 0.0f, p_theme->text_muted); Rectangle search = Canvas_Lucide_Search_Bounds(p_entity); DrawRectangleRounded( search, 0.24f, 10, p_entity->active ? p_theme->surface : p_theme->surface_muted); DrawRectangleRoundedLinesEx( search, 0.24f, 10, (p_entity->active ? 2.0f : 1.0f) / zoom, p_entity->active ? p_theme->accent : p_theme->border); const char *p_search_text = p_entity->text[0] ? p_entity->text : "Search icons"; DrawTextEx( font, p_search_text, (Vector2){search.x + 16.0f, search.y + 13.0f}, 14.0f, 0.0f, p_entity->text[0] ? p_theme->text : p_theme->text_muted); if (p_entity->active) { Vector2 query_size = MeasureTextEx( font, p_entity->text, 14.0f, 0.0f); float caret_x = search.x + 16.0f + query_size.x + 1.0f; DrawLineEx( (Vector2){caret_x, search.y + 11.0f}, (Vector2){caret_x, search.y + 31.0f}, 1.0f / zoom, p_theme->accent); } DrawLineEx( (Vector2){bounds.x, bounds.y + CANVAS_LUCIDE_HEADER_HEIGHT}, (Vector2){ bounds.x + bounds.width, bounds.y + CANVAS_LUCIDE_HEADER_HEIGHT, }, 1.0f / zoom, p_theme->border); float content_top = bounds.y + CANVAS_LUCIDE_HEADER_HEIGHT; float content_bottom = bounds.y + bounds.height - CANVAS_LUCIDE_PADDING; int32 visible_index = 0; for (int32 icon_index = 0; icon_index < CANVAS_LUCIDE_ICON_COUNT; icon_index++) { const Canvas_Lucide_Icon *p_icon = &CANVAS_LUCIDE_ICONS[icon_index]; if (!Canvas_Lucide_Name_Matches( p_icon->p_name, p_entity->text)) { continue; } int32 row = visible_index / CANVAS_LUCIDE_COLUMNS; int32 column = visible_index % CANVAS_LUCIDE_COLUMNS; visible_index++; float card_x = bounds.x + CANVAS_LUCIDE_PADDING + (float)column * (CANVAS_LUCIDE_CARD_WIDTH + CANVAS_LUCIDE_GAP); float card_y = content_top + CANVAS_LUCIDE_PADDING + (float)row * (CANVAS_LUCIDE_CARD_HEIGHT + CANVAS_LUCIDE_GAP) - (float)p_entity->value; if (card_y < content_top || card_y + CANVAS_LUCIDE_CARD_HEIGHT > content_bottom) { continue; } Rectangle card = { card_x, card_y, CANVAS_LUCIDE_CARD_WIDTH, CANVAS_LUCIDE_CARD_HEIGHT, }; DrawRectangleRounded(card, 0.10f, 8, p_theme->surface_muted); DrawRectangleRoundedLinesEx( card, 0.10f, 8, 1.0f / zoom, p_theme->border); float icon_scale = 2.0f; Vector2 icon_origin = { card.x + (card.width - 24.0f * icon_scale) * 0.5f, card.y + 13.0f, }; Canvas_Lucide_Draw_Icon( p_icon, icon_origin, icon_scale, 1.8f, p_theme->text); Vector2 label_size = MeasureTextEx( font, p_icon->p_name, 11.0f, 0.0f); float label_x = card.x + (card.width - label_size.x) * 0.5f; if (label_x < card.x + 5.0f) label_x = card.x + 5.0f; DrawTextEx( font, p_icon->p_name, (Vector2){label_x, card.y + 73.0f}, 11.0f, 0.0f, p_theme->text_muted); } float max_scroll = Canvas_Lucide_Max_Scroll(p_entity); if (max_scroll > 0.0f) { float track_height = bounds.height - CANVAS_LUCIDE_HEADER_HEIGHT - 24.0f; float thumb_height = fmaxf( 36.0f, track_height * bounds.height / Canvas_Lucide_Content_Height(p_entity->text)); float thumb_y = content_top + 12.0f + ((float)p_entity->value / max_scroll) * (track_height - thumb_height); DrawRectangleRounded( (Rectangle){ bounds.x + bounds.width - 10.0f, thumb_y, 5.0f, thumb_height, }, 0.50f, 6, Canvas_Color_Fade(p_theme->text_muted, 0.72f)); } break; } default: break; } if (p_entity->pinned) { const Canvas_Lucide_Icon *p_pin = Canvas_Lucide_Find_Icon("pin"); float scale = 0.72f / zoom; Vector2 origin = { p_entity->position.x + p_entity->size.x - 21.0f / zoom, p_entity->position.y + 3.0f / zoom, }; Canvas_Lucide_Draw_Icon( p_pin, origin, scale, 1.8f / zoom, p_theme->pin); } if (selected && p_entity->type != CANVAS_ENTITY_NOTIFICATION) { Canvas_Draw_Selection(p_entity, zoom, p_theme->selection); } } static void Canvas_Draw_Dropdown_Menu( const Canvas_Entity *p_entity, Font font, Vector2 world_pointer, float zoom, const Canvas_Theme *p_theme) { float opacity = Canvas_Entity_Visibility(p_entity); if (opacity <= 0.001f) return; Canvas_Theme faded_theme = Canvas_Theme_With_Opacity(p_theme, opacity); p_theme = &faded_theme; static const char *options[] = { "Design", "Engineering", "Research", }; Rectangle menu = { p_entity->position.x, p_entity->position.y + p_entity->size.y + 8.0f, p_entity->size.x, 120.0f, }; Canvas_Theme_Draw_Shadow(p_theme, menu, 0.12f, 14, zoom, 1.0f); DrawRectangleRounded(menu, 0.12f, 14, p_theme->surface); DrawRectangleRoundedLinesEx( menu, 0.12f, 14, 1.0f / zoom, p_theme->border); for (int32 option = 0; option < 3; option++) { Rectangle row = { menu.x + 5.0f, menu.y + 4.0f + (float)option * 38.0f, menu.width - 10.0f, 36.0f, }; boolean hovered = CheckCollisionPointRec(world_pointer, row) ? TRUE : FALSE; boolean selected = p_entity->value == option; if (hovered || selected) { DrawRectangleRounded( row, 0.20f, 10, hovered ? p_theme->accent_soft : p_theme->surface_muted); } DrawTextEx( font, options[option], (Vector2){row.x + 12.0f, row.y + 9.0f}, 15.0f, 0.1f, selected ? p_theme->accent : p_theme->text); if (selected) { DrawCircleV( (Vector2){row.x + row.width - 14.0f, row.y + row.height * 0.5f}, 3.0f, p_theme->accent); } } } static Camera2D Canvas_Raylib_Camera(const Canvas_Camera *p_camera) { return (Camera2D){ .offset = { (float)p_camera->viewport_width * 0.5f, (float)p_camera->viewport_height * 0.5f, }, .target = p_camera->target, .rotation = 0.0f, .zoom = p_camera->zoom, }; } void Canvas_Draw_World_Background( const Canvas_Camera *p_camera, const Canvas_Scene *p_scene, const Canvas_Theme *p_theme) { BeginMode2D(Canvas_Raylib_Camera(p_camera)); if (p_scene->show_grid) Canvas_Draw_Grid(p_camera, p_theme); EndMode2D(); } void Canvas_Draw_World_Entity( const Canvas_Camera *p_camera, const Canvas_Scene *p_scene, size_t entity_index, Font font, const Canvas_Theme *p_theme) { if (entity_index >= Dowa_Array_Length(p_scene->p_entities)) return; BeginMode2D(Canvas_Raylib_Camera(p_camera)); Canvas_Draw_Entity( &p_scene->p_entities[entity_index], font, p_camera->zoom, (int32)entity_index == p_scene->selected_index, (int32)entity_index == p_scene->pressed_index && IsMouseButtonDown(MOUSE_BUTTON_LEFT), p_theme); EndMode2D(); } void Canvas_Draw_Web_Chrome_Entity( const Canvas_Camera *p_camera, const Canvas_Scene *p_scene, size_t entity_index, Font font, const Canvas_Theme *p_theme, const char *p_url, boolean can_go_back, boolean can_go_forward) { if (entity_index >= Dowa_Array_Length(p_scene->p_entities)) return; const Canvas_Entity *p_entity = &p_scene->p_entities[entity_index]; if (p_entity->type != CANVAS_ENTITY_WEB_CONTENT || p_entity->web_chrome_amount <= 0.01f) { return; } float zoom = p_camera->zoom; float opacity = Canvas_Entity_Visibility(p_entity); Canvas_Theme faded_theme = Canvas_Theme_With_Opacity(p_theme, opacity); p_theme = &faded_theme; Rectangle toolbar = Canvas_Web_Toolbar_Bounds(p_camera, p_entity); Rectangle back = Canvas_Web_Toolbar_Button_Bounds(toolbar, zoom, 0); Rectangle forward = Canvas_Web_Toolbar_Button_Bounds(toolbar, zoom, 1); Rectangle address = Canvas_Web_Address_Bounds(toolbar, zoom); Vector2 pointer = Canvas_Camera_Screen_To_World( p_camera, GetMousePosition()); BeginMode2D(Canvas_Raylib_Camera(p_camera)); Vector2 toolbar_screen = Canvas_Camera_World_To_Screen( p_camera, (Vector2){toolbar.x, toolbar.y}); BeginScissorMode( (int32)toolbar_screen.x, (int32)toolbar_screen.y, (int32)(toolbar.width * zoom), (int32)fmaxf(1.0f, toolbar.height * zoom)); DrawRectangleRounded(toolbar, 0.18f, 10, p_theme->surface); DrawRectangleRoundedLinesEx( toolbar, 0.18f, 10, 1.0f / zoom, p_theme->border); Rectangle buttons[] = {back, forward}; boolean enabled[] = {can_go_back, can_go_forward}; const char *icon_names[] = {"arrow-left", "arrow-right"}; for (int32 index = 0; index < 2; index++) { boolean hovered = CheckCollisionPointRec(pointer, buttons[index]) && enabled[index]; if (hovered) { DrawRectangleRounded( buttons[index], 0.28f, 8, p_theme->accent_soft); } Canvas_Lucide_Draw_Icon( Canvas_Lucide_Find_Icon(icon_names[index]), (Vector2){ buttons[index].x + 5.5f / zoom, buttons[index].y + 5.5f / zoom, }, 0.71f / zoom, 1.8f / zoom, enabled[index] ? p_theme->text : p_theme->text_muted); } DrawRectangleRounded(address, 0.32f, 8, p_theme->surface_muted); DrawRectangleRoundedLinesEx( address, 0.32f, 8, 1.0f / zoom, p_theme->border); boolean editing = p_scene->editing_web_url && p_scene->web_url_entity_id == p_entity->id; const char *p_display_url = editing ? p_scene->web_url_draft : p_url; char visible_url[CANVAS_ENTITY_TEXT_CAPACITY] = {0}; snprintf( visible_url, sizeof(visible_url), "%s", p_display_url && p_display_url[0] ? p_display_url : "about:blank"); float font_size = 13.0f / zoom; float max_width = address.width - 18.0f / zoom; size_t url_length = strlen(visible_url); while (!editing && url_length > 3 && MeasureTextEx(font, visible_url, font_size, 0.0f).x > max_width) { url_length--; visible_url[url_length] = '\0'; } if (!editing && p_display_url && strlen(p_display_url) > url_length && url_length > 3) { visible_url[url_length - 3] = '.'; visible_url[url_length - 2] = '.'; visible_url[url_length - 1] = '.'; } Vector2 address_screen = Canvas_Camera_World_To_Screen( p_camera, (Vector2){address.x, address.y}); float address_clip_height = fminf( address.height, toolbar.y + toolbar.height - address.y); if (address_clip_height < 1.0f / zoom) { EndScissorMode(); EndMode2D(); return; } BeginScissorMode( (int32)address_screen.x, (int32)address_screen.y, (int32)(address.width * zoom), (int32)(address_clip_height * zoom)); float text_x = address.x + 9.0f / zoom - (editing ? p_scene->web_url_scroll_x : 0.0f); if (editing && p_scene->web_url_cursor != p_scene->web_url_selection_anchor) { int32 selection_start = 0; int32 selection_end = 0; Canvas_Web_URL_Selection_Bounds( p_scene, &selection_start, &selection_end); char prefix[CANVAS_ENTITY_TEXT_CAPACITY]; char selected[CANVAS_ENTITY_TEXT_CAPACITY]; memcpy(prefix, visible_url, (size_t)selection_start); prefix[selection_start] = '\0'; memcpy( selected, visible_url + selection_start, (size_t)(selection_end - selection_start)); selected[selection_end - selection_start] = '\0'; float selection_x = text_x + MeasureTextEx(font, prefix, font_size, 0.0f).x; float selection_width = MeasureTextEx(font, selected, font_size, 0.0f).x; DrawRectangleRec( (Rectangle){ selection_x, address.y + 5.0f / zoom, selection_width, 18.0f / zoom, }, p_theme->accent_soft); } DrawTextEx( font, visible_url, (Vector2){ text_x, address.y + 7.0f / zoom, }, font_size, 0.0f, editing ? p_theme->text : p_theme->text_muted); if (editing && ((int32)(GetTime() * 2.0) % 2) == 0) { char prefix[CANVAS_ENTITY_TEXT_CAPACITY]; memcpy( prefix, visible_url, (size_t)p_scene->web_url_cursor); prefix[p_scene->web_url_cursor] = '\0'; float caret_x = text_x + MeasureTextEx(font, prefix, font_size, 0.0f).x; DrawLineEx( (Vector2){caret_x, address.y + 6.0f / zoom}, (Vector2){caret_x, address.y + 22.0f / zoom}, 1.5f / zoom, p_theme->accent); } EndScissorMode(); EndMode2D(); } void Canvas_Draw_World_Overlays( const Canvas_Camera *p_camera, const Canvas_Scene *p_scene, Font font, const Canvas_Theme *p_theme) { Vector2 world_pointer = Canvas_Camera_Screen_To_World(p_camera, GetMousePosition()); BeginMode2D(Canvas_Raylib_Camera(p_camera)); for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { const Canvas_Entity *p_entity = &p_scene->p_entities[index]; if (p_entity->type == CANVAS_ENTITY_DROPDOWN && p_entity->active) { Canvas_Draw_Dropdown_Menu( p_entity, font, world_pointer, p_camera->zoom, p_theme); } } EndMode2D(); } void Canvas_Draw_World( const Canvas_Camera *p_camera, const Canvas_Scene *p_scene, Font font, const Canvas_Theme *p_theme) { Canvas_Draw_World_Background(p_camera, p_scene, p_theme); for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) { Canvas_Draw_World_Entity( p_camera, p_scene, index, font, p_theme); } Canvas_Draw_World_Overlays(p_camera, p_scene, font, p_theme); } const char *Canvas_Entity_Type_Name(Canvas_Entity_Type type) { switch (type) { case CANVAS_ENTITY_RECTANGLE: return "Rectangle"; case CANVAS_ENTITY_CIRCLE: return "Circle"; case CANVAS_ENTITY_LINE: return "Line"; case CANVAS_ENTITY_TEXT: return "Text"; case CANVAS_ENTITY_BUTTON: return "Button"; case CANVAS_ENTITY_TEXT_AREA: return "Text area"; case CANVAS_ENTITY_DROPDOWN: return "Dropdown"; case CANVAS_ENTITY_ACCORDION: return "Accordion"; case CANVAS_ENTITY_CARD: return "Card"; case CANVAS_ENTITY_CALENDAR: return "Calendar"; case CANVAS_ENTITY_SWITCH: return "Switch"; case CANVAS_ENTITY_TABLE: return "Table"; case CANVAS_ENTITY_NOTIFICATION: return "Notification"; case CANVAS_ENTITY_SCROLL_AREA: return "Scrollable area"; case CANVAS_ENTITY_IMAGE: return "Image"; case CANVAS_ENTITY_WEB_CONTENT: return "Web content"; case CANVAS_ENTITY_CONVERSATION: return "Agent conversation"; case CANVAS_ENTITY_LUCIDE_GALLERY: return "Lucide gallery"; default: return "Unknown"; } }