Mercurial
diff infinite_canvas/main.c @ 281:c57149ad216e default tip
Copilot-Session: f68442b1-fa8f-46a0-9689-81710613bbd4
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Tue, 18 Aug 2026 22:18:15 -0700 |
| parents | 49e9e591c9bb |
| children |
line wrap: on
line diff
--- a/infinite_canvas/main.c Tue Aug 18 19:14:53 2026 -0700 +++ b/infinite_canvas/main.c Tue Aug 18 22:18:15 2026 -0700 @@ -1,5 +1,7 @@ +#include "infinite_canvas/agent_response.h" #include "infinite_canvas/agent_service.h" #include "infinite_canvas/canvas.h" +#include "infinite_canvas/scene_store.h" #include "infinite_canvas/web_surface.h" #if defined(INFINITE_CANVAS_DEV_UI) @@ -27,9 +29,14 @@ const char *p_screenshot_path; uint32 frame_count; uint32 screenshot_frame; - uint32 pending_conversation_id; + Vector2 pending_indicator_position; + Vector2 pending_indicator_drag_offset; uint32 dictation_entity_id; boolean agent_initialized; + boolean pending_indicator_visible; + boolean pending_indicator_hovered; + boolean pending_indicator_dragging; + float pending_indicator_hover_amount; boolean dictation_overlay_visible; boolean dictation_listening; boolean dictation_requested_active; @@ -46,48 +53,6 @@ static Canvas_App *g_app = NULL; -static const char *Canvas_App_JSON_String( - const char *p_json, - const char *p_key, - char *p_output, - size_t output_capacity) -{ - char needle[128]; - snprintf(needle, sizeof(needle), "\"%s\"", p_key); - const char *p_cursor = strstr(p_json, needle); - if (!p_cursor) return NULL; - p_cursor = strchr(p_cursor + strlen(needle), ':'); - if (!p_cursor) return NULL; - while (*++p_cursor == ' ') {} - if (*p_cursor != '"') return NULL; - p_cursor++; - size_t output = 0; - while (*p_cursor && *p_cursor != '"' && output + 1 < output_capacity) { - if (*p_cursor == '\\' && p_cursor[1]) { - p_cursor++; - if (*p_cursor == 'n') p_output[output++] = '\n'; - else if (*p_cursor == 't') p_output[output++] = '\t'; - else if (*p_cursor != 'r') p_output[output++] = *p_cursor; - } else { - p_output[output++] = *p_cursor; - } - p_cursor++; - } - p_output[output] = '\0'; - return p_output; -} - -static uint32 Canvas_App_JSON_Uint(const char *p_json, const char *p_key) -{ - char needle[128]; - snprintf(needle, sizeof(needle), "\"%s\"", p_key); - const char *p_cursor = strstr(p_json, needle); - if (!p_cursor) return 0; - p_cursor = strchr(p_cursor + strlen(needle), ':'); - if (!p_cursor) return 0; - return (uint32)strtoul(p_cursor + 1, NULL, 10); -} - static Canvas_Entity *Canvas_App_Find_Entity(uint32 entity_id) { for (size_t index = 0; @@ -100,38 +65,6 @@ return NULL; } -static void Canvas_App_Set_Conversation_Text( - Canvas_Entity *p_entity, - const char *p_prompt, - const char *p_speaker, - const char *p_response) -{ - size_t length = 0; - int32 written = snprintf( - p_entity->text, - sizeof(p_entity->text), - "You\n"); - if (written > 0) length = (size_t)written; - written = snprintf( - p_entity->text + length, - sizeof(p_entity->text) - length, - "%.*s\n\n%s\n", - 900, - p_prompt, - p_speaker); - if (written > 0) length += (size_t)written; - if (length >= sizeof(p_entity->text)) { - length = sizeof(p_entity->text) - 1; - } - snprintf( - p_entity->text + length, - sizeof(p_entity->text) - length, - "%.*s", - 900, - p_response); - Canvas_Conversation_Scroll_To_End(p_entity, g_app->font); -} - static boolean Canvas_App_Submit_Prompt(const char *p_prompt) { if (!p_prompt || !p_prompt[0]) return FALSE; @@ -151,85 +84,140 @@ sizeof(g_app->pending_prompt), "%s", p_prompt); - Vector2 position = Canvas_Camera_Screen_To_World( + g_app->pending_indicator_position = Canvas_Camera_Screen_To_World( &g_app->camera, (Vector2){ - (float)g_app->camera.viewport_width * 0.5f - 210.0f, + (float)g_app->camera.viewport_width * 0.5f + 240.0f, (float)g_app->camera.viewport_height * 0.5f - 150.0f, }); - g_app->pending_conversation_id = Canvas_Scene_Add_Conversation( - &g_app->scene, - "Routing thought...", - p_prompt, - "Copilot is deciding whether to create or extend a session.", - position); - Canvas_Entity *p_pending = - Canvas_App_Find_Entity(g_app->pending_conversation_id); - if (p_pending) p_pending->agent_working = TRUE; + g_app->pending_indicator_visible = TRUE; + g_app->pending_indicator_hovered = FALSE; + g_app->pending_indicator_dragging = FALSE; + g_app->pending_indicator_hover_amount = 0.0f; return TRUE; } +static uint32 Canvas_App_Materialize_Agent_Entities( + Canvas_Agent_Response *p_response) +{ + uint32 created = 0; + for (uint32 index = 0; index < p_response->entity_count; index++) { + Canvas_Agent_Entity_Spec *p_spec = &p_response->entities[index]; + Vector2 position = p_spec->has_position + ? p_spec->position + : (Vector2){ + g_app->pending_indicator_position.x + + (float)(index % 2) * 280.0f, + g_app->pending_indicator_position.y + + (float)(index / 2) * 240.0f, + }; + if (!Canvas_Scene_Add(&g_app->scene, p_spec->type, position)) { + break; + } + Canvas_Entity *p_entity = &g_app->scene.p_entities[ + Dowa_Array_Length(g_app->scene.p_entities) - 1]; + if (p_spec->label[0]) { + snprintf( + p_entity->label, + sizeof(p_entity->label), + "%s", + p_spec->label); + } + if (p_spec->text[0]) { + snprintf( + p_entity->text, + sizeof(p_entity->text), + "%s", + p_spec->text); + p_entity->text_cursor = (int32)strlen(p_entity->text); + p_entity->text_selection_anchor = p_entity->text_cursor; + } + if (p_spec->has_size) p_entity->size = p_spec->size; + if (p_spec->has_value) p_entity->value = p_spec->value; + created++; + } + return created; +} + +static void Canvas_App_Show_Agent_Notification( + const char *p_title, + const char *p_text) +{ + if (!Canvas_Scene_Add( + &g_app->scene, + CANVAS_ENTITY_NOTIFICATION, + g_app->pending_indicator_position)) { + return; + } + Canvas_Entity *p_entity = &g_app->scene.p_entities[ + Dowa_Array_Length(g_app->scene.p_entities) - 1]; + if (p_title && p_title[0]) { + snprintf( + p_entity->label, + sizeof(p_entity->label), + "%s", + p_title); + } + snprintf( + p_entity->text, + sizeof(p_entity->text), + "%s", + p_text ? p_text : ""); +} + static void Canvas_App_Apply_Agent_Response(const char *p_json) { - char action[32] = {0}; - char title[128] = {0}; - char response[CANVAS_ENTITY_TEXT_CAPACITY] = {0}; - Canvas_App_JSON_String(p_json, "action", action, sizeof(action)); - Canvas_App_JSON_String(p_json, "title", title, sizeof(title)); - if (!Canvas_App_JSON_String( - p_json, - "response", - response, - sizeof(response))) { - snprintf(response, sizeof(response), "%s", p_json); + Canvas_Agent_Response parsed; + boolean valid = Canvas_Agent_Response_Parse(p_json, &parsed); + if (!valid) { + memset(&parsed, 0, sizeof(parsed)); + snprintf(parsed.action, sizeof(parsed.action), "create"); + snprintf( + parsed.presentation, + sizeof(parsed.presentation), + "conversation"); + snprintf( + parsed.response, + sizeof(parsed.response), + "%s", + p_json); } - uint32 conversation_id = - Canvas_App_JSON_Uint(p_json, "conversation_id"); - Canvas_Entity *p_pending = - Canvas_App_Find_Entity(g_app->pending_conversation_id); - if (strcmp(action, "append") == 0 && - conversation_id != 0 && + uint32 created_entities = + Canvas_App_Materialize_Agent_Entities(&parsed); + if (strcmp(parsed.action, "append") == 0 && + parsed.conversation_id != 0 && Canvas_Scene_Append_Conversation( &g_app->scene, - conversation_id, + parsed.conversation_id, g_app->pending_prompt, - response)) { + parsed.response)) { Canvas_Entity *p_conversation = - Canvas_App_Find_Entity(conversation_id); + Canvas_App_Find_Entity(parsed.conversation_id); Canvas_Conversation_Scroll_To_End( p_conversation, g_app->font); - if (p_pending) p_pending->removing = TRUE; - } else if (p_pending) { - snprintf( - p_pending->label, - sizeof(p_pending->label), - "%s", - title[0] ? title : "Copilot session"); - Canvas_App_Set_Conversation_Text( - p_pending, - g_app->pending_prompt, - "Copilot", - response); - p_pending->agent_working = FALSE; + } else if (strcmp(parsed.presentation, "action") == 0) { + if (created_entities == 0) { + Canvas_App_Show_Agent_Notification( + parsed.title[0] ? parsed.title : "Copilot", + parsed.response); + } } else { - Vector2 position = Canvas_Camera_Screen_To_World( - &g_app->camera, - (Vector2){ - (float)g_app->camera.viewport_width * 0.5f - 210.0f, - (float)g_app->camera.viewport_height * 0.5f - 150.0f, - }); + Vector2 position = { + g_app->pending_indicator_position.x - 210.0f, + g_app->pending_indicator_position.y - 150.0f, + }; uint32 created_id = Canvas_Scene_Add_Conversation( &g_app->scene, - title[0] ? title : "Copilot session", + parsed.title[0] ? parsed.title : "Copilot session", g_app->pending_prompt, - response, + parsed.response, position); Canvas_Conversation_Scroll_To_End( Canvas_App_Find_Entity(created_id), g_app->font); } - g_app->pending_conversation_id = 0; + g_app->pending_indicator_visible = FALSE; g_app->pending_prompt[0] = '\0'; } @@ -421,6 +409,194 @@ g_app->theme.text_muted); } +static void Canvas_App_Draw_Agent_Indicator(void) +{ + if (!g_app->pending_indicator_visible) return; + Vector2 screen = Canvas_Camera_World_To_Screen( + &g_app->camera, + g_app->pending_indicator_position); + float pulse = 0.5f + 0.5f * sinf((float)GetTime() * 5.0f); + float hover = g_app->pending_indicator_hover_amount; + float radius = 22.0f + hover * 2.0f; + Rectangle bounds = { + screen.x - radius, + screen.y - radius, + radius * 2.0f, + radius * 2.0f, + }; + Canvas_Theme_Draw_Shadow( + &g_app->theme, + bounds, + 0.5f, + 16, + 1.0f, + 1.0f); + DrawCircleV(screen, radius, g_app->theme.surface); + DrawCircleLinesV( + screen, + 20.0f + pulse * 2.0f, + Fade(g_app->theme.accent, 0.45f + pulse * 0.35f)); + Canvas_Draw_Lucide_Icon( + "sparkles", + (Vector2){screen.x - 9.0f, screen.y - 9.0f}, + 0.75f, + 1.8f, + g_app->theme.accent); + if (hover > 0.01f) { + Rectangle label = { + screen.x + 28.0f - (1.0f - hover) * 8.0f, + screen.y - 17.0f, + 118.0f, + 34.0f, + }; + DrawRectangleRounded( + label, + 0.35f, + 10, + Fade(g_app->theme.surface, hover)); + DrawRectangleRoundedLinesEx( + label, + 0.35f, + 10, + 1.0f, + Fade(g_app->theme.border, hover)); + DrawTextEx( + g_app->font, + g_app->pending_indicator_dragging + ? "Move result" + : "Copilot working", + (Vector2){label.x + 12.0f, label.y + 10.0f}, + 13.0f, + 0.0f, + Fade(g_app->theme.text, hover)); + } +} + +static boolean Canvas_App_Update_Agent_Indicator_Input(boolean blocked) +{ + if (!g_app->pending_indicator_visible) { + g_app->pending_indicator_hovered = FALSE; + g_app->pending_indicator_dragging = FALSE; + g_app->pending_indicator_hover_amount = 0.0f; + return FALSE; + } + Vector2 pointer = GetMousePosition(); + Vector2 screen = Canvas_Camera_World_To_Screen( + &g_app->camera, + g_app->pending_indicator_position); + if (g_app->pending_indicator_dragging) { + Vector2 target_screen = { + pointer.x - g_app->pending_indicator_drag_offset.x, + pointer.y - g_app->pending_indicator_drag_offset.y, + }; + g_app->pending_indicator_position = + Canvas_Camera_Screen_To_World( + &g_app->camera, + target_screen); + if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) { + g_app->pending_indicator_dragging = FALSE; + } + g_app->pending_indicator_hovered = TRUE; + } else { + g_app->pending_indicator_hovered = + !blocked && + CheckCollisionPointCircle(pointer, screen, 26.0f); + if (g_app->pending_indicator_hovered && + IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { + g_app->pending_indicator_dragging = TRUE; + g_app->pending_indicator_drag_offset = (Vector2){ + pointer.x - screen.x, + pointer.y - screen.y, + }; + } + } + float target = g_app->pending_indicator_hovered ? 1.0f : 0.0f; + float step = GetFrameTime() * 10.0f; + if (g_app->pending_indicator_hover_amount < target) { + g_app->pending_indicator_hover_amount = fminf( + target, + g_app->pending_indicator_hover_amount + step); + } else { + g_app->pending_indicator_hover_amount = fmaxf( + target, + g_app->pending_indicator_hover_amount - step); + } + return g_app->pending_indicator_hovered || + g_app->pending_indicator_dragging; +} + +#if defined(INFINITE_CANVAS_DEV_UI) +static void Canvas_App_Handle_Dev_Action(Canvas_Dev_UI_Action action) +{ + if (action == CANVAS_DEV_UI_ACTION_NONE) return; + char path[CANVAS_SCENE_STORE_PATH_CAPACITY]; + if (!Canvas_Scene_Store_Path( + g_app->dev_ui.snapshot_name, + path, + sizeof(path))) { + Canvas_Dev_UI_Set_Snapshot_Status( + &g_app->dev_ui, + "Use only letters, numbers, - or _"); + return; + } + if (action == CANVAS_DEV_UI_ACTION_SAVE_SNAPSHOT) { + Canvas_Dev_UI_Set_Snapshot_Status( + &g_app->dev_ui, + Canvas_Scene_Store_Save( + path, + &g_app->scene, + &g_app->camera) + ? "Snapshot saved" + : "Snapshot save failed"); + return; + } + if (g_app->agent_initialized && + g_app->agent_service.status == CANVAS_AGENT_WORKING) { + Canvas_Dev_UI_Set_Snapshot_Status( + &g_app->dev_ui, + "Wait for the active agent turn before loading"); + return; + } + if (!Canvas_Scene_Store_Load( + path, + &g_app->scene, + &g_app->camera)) { + Canvas_Dev_UI_Set_Snapshot_Status( + &g_app->dev_ui, + "Snapshot missing, incompatible, or corrupt"); + return; + } + g_app->pending_indicator_visible = FALSE; + g_app->pending_prompt[0] = '\0'; + g_app->dictation_entity_id = 0; + g_app->dictation_transcript[0] = '\0'; + g_app->dictation_partial[0] = '\0'; + for (size_t index = 0; + index < Dowa_Array_Length(g_app->scene.p_entities); + index++) { + Canvas_Entity *p_entity = &g_app->scene.p_entities[index]; + if (p_entity->type == CANVAS_ENTITY_TEXT_AREA && + strcmp(p_entity->label, "Dictation") == 0) { + g_app->dictation_entity_id = p_entity->id; + snprintf( + p_entity->text_muted + ? g_app->dictation_partial + : g_app->dictation_transcript, + CANVAS_ENTITY_TEXT_CAPACITY, + "%s", + p_entity->text); + break; + } + } + g_app->dictation_overlay_visible = + g_app->dictation_transcript[0] || + g_app->dictation_partial[0]; + Canvas_Dev_UI_Set_Snapshot_Status( + &g_app->dev_ui, + "Snapshot loaded"); +} +#endif + static void Canvas_App_Draw_Frame(void) { int32 width = GetScreenWidth(); @@ -428,13 +604,19 @@ Canvas_Camera_Set_Viewport(&g_app->camera, width, height); boolean block_pointer_input = FALSE; + boolean dev_ui_editing = FALSE; #if defined(INFINITE_CANVAS_DEV_UI) block_pointer_input = Canvas_Dev_UI_Contains_Pointer(&g_app->dev_ui); + dev_ui_editing = Canvas_Dev_UI_Is_Editing(&g_app->dev_ui); #endif + block_pointer_input = + Canvas_App_Update_Agent_Indicator_Input(block_pointer_input) || + block_pointer_input; Canvas_Web_Surface_Set_Dark_Mode( &g_app->web_surface, g_app->theme.resolved_mode == CANVAS_THEME_DARK ? TRUE : FALSE); boolean dictation_hotkey = + !dev_ui_editing && !Canvas_Scene_Is_Text_Editing(&g_app->scene) && !Canvas_Web_Chrome_Is_Editing(&g_app->scene) && IsKeyPressed(KEY_M); @@ -496,6 +678,7 @@ boolean keyboard_focus = Canvas_Scene_Is_Text_Editing(&g_app->scene) || Canvas_Web_Chrome_Is_Editing(&g_app->scene) || + dev_ui_editing || Canvas_Web_Surface_Is_Focused(&g_app->web_surface); if (!keyboard_focus && IsKeyPressed(KEY_P)) { Canvas_Scene_Toggle_Selected_Pin(&g_app->scene, &g_app->camera); @@ -614,31 +797,12 @@ if (agent_status == CANVAS_AGENT_READY) { Canvas_App_Apply_Agent_Response(agent_response); } else if (agent_status == CANVAS_AGENT_ERROR) { - Canvas_Entity *p_pending = - Canvas_App_Find_Entity(g_app->pending_conversation_id); - if (p_pending) { - snprintf(p_pending->label, sizeof(p_pending->label), "Copilot unavailable"); - Canvas_App_Set_Conversation_Text( - p_pending, - g_app->pending_prompt, - "System", + if (g_app->pending_prompt[0]) { + Canvas_App_Show_Agent_Notification( + "Copilot unavailable", agent_error); - p_pending->agent_working = FALSE; - } else if (g_app->pending_prompt[0]) { - Vector2 position = Canvas_Camera_Screen_To_World( - &g_app->camera, - (Vector2){ - (float)g_app->camera.viewport_width * 0.5f - 210.0f, - (float)g_app->camera.viewport_height * 0.5f - 150.0f, - }); - Canvas_Scene_Add_Conversation( - &g_app->scene, - "Copilot unavailable", - g_app->pending_prompt, - agent_error, - position); } - g_app->pending_conversation_id = 0; + g_app->pending_indicator_visible = FALSE; g_app->pending_prompt[0] = '\0'; } @@ -687,8 +851,9 @@ &g_app->scene, g_app->font, &g_app->theme); + Canvas_App_Draw_Agent_Indicator(); #if defined(INFINITE_CANVAS_DEV_UI) - Canvas_Dev_UI_Draw( + Canvas_Dev_UI_Action dev_action = Canvas_Dev_UI_Draw( &g_app->dev_ui, &g_app->camera, &g_app->scene, @@ -697,6 +862,9 @@ #endif Canvas_App_Draw_Listening_Indicator(); EndDrawing(); +#if defined(INFINITE_CANVAS_DEV_UI) + Canvas_App_Handle_Dev_Action(dev_action); +#endif g_app->frame_count++; #if !defined(PLATFORM_WEB) if (g_app->frame_count == 30) {