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