view 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 source

#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)
#include "infinite_canvas/dev_ui.h"
#endif

#if defined(PLATFORM_WEB)
#include <emscripten/emscripten.h>
#endif

#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    Dowa_Arena *p_arena;
    Canvas_Camera camera;
    Canvas_Scene scene;
    Canvas_Web_Surface web_surface;
    Canvas_Agent_Service agent_service;
    Font font;
    Canvas_Theme theme;
    boolean owns_font;
    const char *p_screenshot_path;
    uint32 frame_count;
    uint32 screenshot_frame;
    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;
    boolean dictation_send_pending;
    char dictation_status[128];
    char dictation_partial[CANVAS_ENTITY_TEXT_CAPACITY];
    char dictation_transcript[CANVAS_ENTITY_TEXT_CAPACITY];
    char pending_prompt[CANVAS_ENTITY_TEXT_CAPACITY];
    char agent_context[CANVAS_CONTEXT_MAX_LENGTH];
#if defined(INFINITE_CANVAS_DEV_UI)
    Canvas_Dev_UI dev_ui;
#endif
} Canvas_App;

static Canvas_App *g_app = NULL;

static Canvas_Entity *Canvas_App_Find_Entity(uint32 entity_id)
{
    for (size_t index = 0;
        index < Dowa_Array_Length(g_app->scene.p_entities);
        index++) {
        if (g_app->scene.p_entities[index].id == entity_id) {
            return &g_app->scene.p_entities[index];
        }
    }
    return NULL;
}

static boolean Canvas_App_Submit_Prompt(const char *p_prompt)
{
    if (!p_prompt || !p_prompt[0]) return FALSE;
    Canvas_Scene_Build_Visible_Context(
        &g_app->scene,
        &g_app->camera,
        g_app->agent_context,
        sizeof(g_app->agent_context));
    if (!Canvas_Agent_Service_Submit(
            &g_app->agent_service,
            p_prompt,
            g_app->agent_context)) {
        return FALSE;
    }
    snprintf(
        g_app->pending_prompt,
        sizeof(g_app->pending_prompt),
        "%s",
        p_prompt);
    g_app->pending_indicator_position = Canvas_Camera_Screen_To_World(
        &g_app->camera,
        (Vector2){
            (float)g_app->camera.viewport_width * 0.5f + 240.0f,
            (float)g_app->camera.viewport_height * 0.5f - 150.0f,
        });
    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)
{
    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 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,
            parsed.conversation_id,
            g_app->pending_prompt,
            parsed.response)) {
        Canvas_Entity *p_conversation =
            Canvas_App_Find_Entity(parsed.conversation_id);
        Canvas_Conversation_Scroll_To_End(
            p_conversation,
            g_app->font);
    } 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 = {
            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,
            parsed.title[0] ? parsed.title : "Copilot session",
            g_app->pending_prompt,
            parsed.response,
            position);
        Canvas_Conversation_Scroll_To_End(
            Canvas_App_Find_Entity(created_id),
            g_app->font);
    }
    g_app->pending_indicator_visible = FALSE;
    g_app->pending_prompt[0] = '\0';
}

static Canvas_Entity *Canvas_App_Ensure_Dictation_Entity(void)
{
    Canvas_Entity *p_entity =
        Canvas_App_Find_Entity(g_app->dictation_entity_id);
    if (p_entity) return p_entity;

    Vector2 center = Canvas_Camera_Screen_To_World(
        &g_app->camera,
        (Vector2){
            (float)g_app->camera.viewport_width * 0.5f,
            (float)g_app->camera.viewport_height * 0.5f,
        });
    Vector2 size = {520.0f, 240.0f};
    if (!Canvas_Scene_Add(
            &g_app->scene,
            CANVAS_ENTITY_TEXT_AREA,
            (Vector2){
                center.x - size.x * 0.5f,
                center.y - size.y * 0.5f,
            })) {
        return NULL;
    }
    size_t index = Dowa_Array_Length(g_app->scene.p_entities) - 1;
    p_entity = &g_app->scene.p_entities[index];
    p_entity->size = size;
    snprintf(p_entity->label, sizeof(p_entity->label), "Dictation");
    g_app->dictation_entity_id = p_entity->id;
    g_app->scene.selected_index = (int32)index;
    return p_entity;
}

static void Canvas_App_Sync_Dictation_Entity(void)
{
    Canvas_Entity *p_entity =
        Canvas_App_Find_Entity(g_app->dictation_entity_id);
    if (!p_entity) return;

    char text[CANVAS_ENTITY_TEXT_CAPACITY];
    snprintf(text, sizeof(text), "%s", g_app->dictation_transcript);
    size_t length = strlen(text);
    if (length > 0 && g_app->dictation_partial[0] &&
        length + 1 < sizeof(text)) {
        text[length++] = ' ';
        text[length] = '\0';
    }
    snprintf(
        text + length,
        sizeof(text) - length,
        "%s",
        g_app->dictation_partial);
    Canvas_Text_Area_Set_Content(
        p_entity,
        g_app->font,
        text,
        g_app->dictation_partial[0] ? TRUE : FALSE);
}

static void Canvas_App_Set_Dictation_Active(boolean active)
{
    g_app->dictation_overlay_visible = TRUE;
    g_app->dictation_requested_active = active;
    g_app->dictation_listening = FALSE;
    g_app->dictation_send_pending = FALSE;
    if (active) {
        Canvas_App_Ensure_Dictation_Entity();
        Canvas_App_Sync_Dictation_Entity();
    }
    snprintf(
        g_app->dictation_status,
        sizeof(g_app->dictation_status),
        "%s",
        active ? "Starting microphone..." : "Stopping microphone...");
    Canvas_Web_Surface_Set_Dictation_Active(
        &g_app->web_surface,
        active);
}

static boolean Canvas_App_Submit_Dictation(void)
{
    if (!g_app->dictation_transcript[0]) return FALSE;
    if (!Canvas_App_Submit_Prompt(g_app->dictation_transcript)) {
        g_app->dictation_send_pending = FALSE;
        snprintf(
            g_app->dictation_status,
            sizeof(g_app->dictation_status),
            "Copilot is busy; press Enter to retry");
        return FALSE;
    }
    Canvas_Entity *p_entity =
        Canvas_App_Find_Entity(g_app->dictation_entity_id);
    if (p_entity) p_entity->text_muted = FALSE;
    g_app->dictation_transcript[0] = '\0';
    g_app->dictation_partial[0] = '\0';
    Canvas_App_Sync_Dictation_Entity();
    g_app->dictation_send_pending = FALSE;
    g_app->dictation_overlay_visible =
        g_app->dictation_listening;
    return TRUE;
}

static void Canvas_App_Append_Dictation(const char *p_text)
{
    if (!p_text || !p_text[0]) return;
    size_t length = strlen(g_app->dictation_transcript);
    if (length > 0 && length + 1 < sizeof(g_app->dictation_transcript)) {
        g_app->dictation_transcript[length++] = ' ';
        g_app->dictation_transcript[length] = '\0';
    }
    size_t available = sizeof(g_app->dictation_transcript) - length - 1;
    size_t text_length = strlen(p_text);
    size_t copy_length = text_length < available ? text_length : available;
    memcpy(g_app->dictation_transcript + length, p_text, copy_length);
    g_app->dictation_transcript[length + copy_length] = '\0';
    Canvas_App_Sync_Dictation_Entity();
}

static void Canvas_App_Update_Dictation_Overlay_Input(void)
{
    if (!g_app->dictation_overlay_visible) return;
    if (g_app->dictation_listening && IsKeyPressed(KEY_BACKSPACE)) {
        g_app->dictation_send_pending = FALSE;
        g_app->dictation_partial[0] = '\0';
        g_app->dictation_transcript[0] = '\0';
        Canvas_App_Sync_Dictation_Entity();
        snprintf(
            g_app->dictation_status,
            sizeof(g_app->dictation_status),
            "Listening");
        return;
    }
    if (IsKeyPressed(KEY_ESCAPE)) {
        if (g_app->dictation_listening) {
            Canvas_Web_Surface_Set_Dictation_Active(
                &g_app->web_surface,
                FALSE);
            g_app->dictation_requested_active = FALSE;
            g_app->dictation_listening = FALSE;
        }
        g_app->dictation_send_pending = FALSE;
        g_app->dictation_partial[0] = '\0';
        g_app->dictation_transcript[0] = '\0';
        Canvas_App_Sync_Dictation_Entity();
        g_app->dictation_overlay_visible = FALSE;
        return;
    }
    if (!IsKeyPressed(KEY_ENTER)) return;

    if (g_app->dictation_partial[0]) {
        g_app->dictation_send_pending = TRUE;
        snprintf(
            g_app->dictation_status,
            sizeof(g_app->dictation_status),
            "Finishing dictation...");
        Canvas_Web_Surface_Commit_Dictation(
            &g_app->web_surface);
    } else if (g_app->dictation_transcript[0]) {
        Canvas_App_Submit_Dictation();
    }
}

static boolean Canvas_App_Dictation_Status_Is_Error(const char *p_status)
{
    return strstr(p_status, "failed") != NULL ||
        strstr(p_status, "Unable") != NULL ||
        strstr(p_status, "closed") != NULL ||
        strstr(p_status, "invalid") != NULL ||
        strstr(p_status, "error") != NULL;
}

static void Canvas_App_Draw_Listening_Indicator(void)
{
    if (!g_app->dictation_listening) return;

    float pulse = 0.72f + 0.28f * sinf((float)GetTime() * 5.0f);
    Vector2 dot = {28.0f, (float)GetScreenHeight() - 28.0f};
    DrawCircleV(
        dot,
        5.0f + pulse * 2.0f,
        Fade(g_app->theme.danger, 0.82f));
    DrawTextEx(
        g_app->font,
        "Listening",
        (Vector2){44.0f, dot.y - 8.0f},
        14.0f,
        0.0f,
        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();
    int32 height = GetScreenHeight();
    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);
    if (dictation_hotkey) {
        Canvas_App_Set_Dictation_Active(
            g_app->dictation_requested_active ? FALSE : TRUE);
    }
    Canvas_App_Update_Dictation_Overlay_Input();
    Canvas_Web_Chrome_Action chrome_action = CANVAS_WEB_CHROME_NONE;
    if (!block_pointer_input) {
        const char *p_current_url = NULL;
        if (g_app->scene.selected_index >= 0 &&
            g_app->scene.selected_index <
                (int32)Dowa_Array_Length(g_app->scene.p_entities)) {
            const Canvas_Entity *p_selected =
                &g_app->scene.p_entities[g_app->scene.selected_index];
            p_current_url = Canvas_Web_Surface_URL(
                &g_app->web_surface,
                p_selected->id);
        }
        chrome_action = Canvas_Web_Chrome_Update_Input(
            &g_app->camera,
            &g_app->scene,
            g_app->font,
            p_current_url);
    }
    if (chrome_action == CANVAS_WEB_CHROME_BACK ||
        chrome_action == CANVAS_WEB_CHROME_FORWARD) {
        const Canvas_Entity *p_selected =
            &g_app->scene.p_entities[g_app->scene.selected_index];
        if (chrome_action == CANVAS_WEB_CHROME_BACK) {
            Canvas_Web_Surface_Go_Back(
                &g_app->web_surface,
                p_selected->id);
        } else {
            Canvas_Web_Surface_Go_Forward(
                &g_app->web_surface,
                p_selected->id);
        }
    }
    boolean web_input = FALSE;
    if (!block_pointer_input &&
        !dictation_hotkey &&
        chrome_action == CANVAS_WEB_CHROME_NONE) {
        web_input = Canvas_Web_Surface_Update_Input(
            &g_app->web_surface,
            &g_app->camera,
            &g_app->scene);
    }
    block_pointer_input =
        block_pointer_input ||
        chrome_action != CANVAS_WEB_CHROME_NONE ||
        web_input;
    boolean entity_input = Canvas_Scene_Update_Interaction(
        &g_app->scene,
        &g_app->camera,
        g_app->font,
        block_pointer_input);
    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);
    }
    if (!keyboard_focus &&
        !g_app->dictation_listening &&
        (IsKeyPressed(KEY_DELETE) || IsKeyPressed(KEY_BACKSPACE))) {
        Canvas_Scene_Fade_Out_Selected(&g_app->scene);
    }
    Canvas_Update_Camera_Input(
        &g_app->camera,
        block_pointer_input || entity_input,
        keyboard_focus);
    Canvas_Scene_Sync_Pinned(&g_app->scene, &g_app->camera);
    Canvas_Scene_Update_Notification_Stack(&g_app->scene, &g_app->camera);

    char dictation_text[CANVAS_ENTITY_TEXT_CAPACITY];
    Canvas_Dictation_Event dictation_event =
        Canvas_Web_Surface_Consume_Dictation(
            &g_app->web_surface,
            dictation_text,
            sizeof(dictation_text));
    if (dictation_event == CANVAS_DICTATION_EVENT_STATUS) {
        snprintf(
            g_app->dictation_status,
            sizeof(g_app->dictation_status),
            "%.127s",
            dictation_text);
        if (strcmp(dictation_text, "Idle") == 0) {
            g_app->dictation_listening = FALSE;
            if (g_app->dictation_send_pending) {
                if (g_app->dictation_partial[0]) {
                    Canvas_App_Append_Dictation(
                        g_app->dictation_partial);
                    g_app->dictation_partial[0] = '\0';
                }
                Canvas_App_Submit_Dictation();
            } else if (!g_app->dictation_transcript[0] &&
                !g_app->dictation_partial[0]) {
                g_app->dictation_overlay_visible = FALSE;
            }
        } else if (strcmp(dictation_text, "Listening") == 0 ||
            strcmp(dictation_text, "Speech detected") == 0) {
            g_app->dictation_listening =
                g_app->dictation_requested_active;
            if (strcmp(dictation_text, "Listening") == 0 &&
                g_app->dictation_send_pending) {
                if (g_app->dictation_partial[0]) {
                    Canvas_App_Append_Dictation(
                        g_app->dictation_partial);
                    g_app->dictation_partial[0] = '\0';
                }
                Canvas_App_Submit_Dictation();
            }
        } else if (Canvas_App_Dictation_Status_Is_Error(dictation_text)) {
            g_app->dictation_listening = FALSE;
            g_app->dictation_overlay_visible = TRUE;
        }
    } else if (dictation_event == CANVAS_DICTATION_EVENT_PARTIAL) {
        g_app->dictation_overlay_visible = TRUE;
        snprintf(
            g_app->dictation_partial,
            sizeof(g_app->dictation_partial),
            "%s",
            dictation_text);
        Canvas_App_Sync_Dictation_Entity();
    } else if (dictation_event == CANVAS_DICTATION_EVENT_FINAL) {
        g_app->dictation_overlay_visible = TRUE;
        Canvas_App_Append_Dictation(dictation_text);
        g_app->dictation_partial[0] = '\0';
        Canvas_App_Sync_Dictation_Entity();
        if (g_app->dictation_send_pending) {
            Canvas_App_Submit_Dictation();
        } else {
            snprintf(
                g_app->dictation_status,
                sizeof(g_app->dictation_status),
                "Ready to send");
        }
    }
    if (!g_app->dictation_overlay_visible &&
        g_app->scene.selected_index >= 0 &&
        g_app->scene.selected_index <
            (int32)Dowa_Array_Length(g_app->scene.p_entities) &&
        (IsKeyDown(KEY_LEFT_CONTROL) ||
            IsKeyDown(KEY_RIGHT_CONTROL) ||
            IsKeyDown(KEY_LEFT_SUPER) ||
            IsKeyDown(KEY_RIGHT_SUPER)) &&
        IsKeyPressed(KEY_ENTER)) {
        Canvas_Entity *p_selected =
            &g_app->scene.p_entities[g_app->scene.selected_index];
        if (p_selected->type == CANVAS_ENTITY_TEXT_AREA &&
            p_selected->text[0]) {
            size_t length = strlen(p_selected->text);
            if (length > 0 && p_selected->text[length - 1] == '\n') {
                p_selected->text[length - 1] = '\0';
            }
            if (Canvas_App_Submit_Prompt(p_selected->text)) {
                p_selected->text[0] = '\0';
                p_selected->text_cursor = 0;
                p_selected->text_selection_anchor = 0;
            }
        }
    }

    char agent_response[CANVAS_AGENT_RESPONSE_CAPACITY];
    char agent_error[512];
    Canvas_Agent_Status agent_status = g_app->agent_initialized ?
        Canvas_Agent_Service_Poll(
            &g_app->agent_service,
            agent_response,
            sizeof(agent_response),
            agent_error,
            sizeof(agent_error)) :
        CANVAS_AGENT_IDLE;
    if (agent_status == CANVAS_AGENT_READY) {
        Canvas_App_Apply_Agent_Response(agent_response);
    } else if (agent_status == CANVAS_AGENT_ERROR) {
        if (g_app->pending_prompt[0]) {
            Canvas_App_Show_Agent_Notification(
                "Copilot unavailable",
                agent_error);
        }
        g_app->pending_indicator_visible = FALSE;
        g_app->pending_prompt[0] = '\0';
    }

    Canvas_Web_Surface_Sync(&g_app->web_surface, &g_app->camera, &g_app->scene);

    BeginDrawing();
    ClearBackground(g_app->theme.background);
    Canvas_Draw_World_Background(
        &g_app->camera,
        &g_app->scene,
        &g_app->theme);
    for (size_t index = 0;
        index < Dowa_Array_Length(g_app->scene.p_entities);
        index++) {
        Canvas_Draw_World_Entity(
            &g_app->camera,
            &g_app->scene,
            index,
            g_app->font,
            &g_app->theme);
        Canvas_Web_Surface_Draw_Entity(
            &g_app->web_surface,
            &g_app->camera,
            &g_app->scene,
            index);
        const Canvas_Entity *p_entity = &g_app->scene.p_entities[index];
        const char *p_url = Canvas_Web_Surface_URL(
            &g_app->web_surface,
            p_entity->id);
        Canvas_Draw_Web_Chrome_Entity(
            &g_app->camera,
            &g_app->scene,
            index,
            g_app->font,
            &g_app->theme,
            p_url ? p_url : p_entity->text,
            Canvas_Web_Surface_Can_Go_Back(
                &g_app->web_surface,
                p_entity->id),
            Canvas_Web_Surface_Can_Go_Forward(
                &g_app->web_surface,
                p_entity->id));
    }
    Canvas_Draw_World_Overlays(
        &g_app->camera,
        &g_app->scene,
        g_app->font,
        &g_app->theme);
    Canvas_App_Draw_Agent_Indicator();
#if defined(INFINITE_CANVAS_DEV_UI)
    Canvas_Dev_UI_Action dev_action = Canvas_Dev_UI_Draw(
        &g_app->dev_ui,
        &g_app->camera,
        &g_app->scene,
        g_app->font,
        &g_app->theme);
#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) {
        RestoreWindow();
        SetWindowFocused();
    }
#endif
    if (g_app->p_screenshot_path &&
        g_app->frame_count == g_app->screenshot_frame) {
        TakeScreenshot(g_app->p_screenshot_path);
    }
}

int main(int argc, char **p_argv)
{
#if !defined(PLATFORM_WEB)
    int32 subprocess_result = Canvas_Web_Surface_Execute_Subprocess(argc, p_argv);
    if (subprocess_result >= 0) return subprocess_result;
#else
    (void)argc;
    (void)p_argv;
#endif

    Dowa_Arena *p_arena = Dowa_Arena_Create(8 * ONE_MEGA_BYTE);
    if (!p_arena) return 1;

    g_app = (Canvas_App *)Dowa_Arena_Allocate(p_arena, sizeof(*g_app));
    if (!g_app) {
        Dowa_Arena_Free(p_arena);
        return 1;
    }
    memset(g_app, 0, sizeof(*g_app));
    g_app->p_arena = p_arena;
    snprintf(
        g_app->dictation_status,
        sizeof(g_app->dictation_status),
        "Idle");
    g_app->p_screenshot_path = getenv("INFINITE_CANVAS_SCREENSHOT");
    g_app->screenshot_frame = 180;
    const char *p_screenshot_frame =
        getenv("INFINITE_CANVAS_SCREENSHOT_FRAME");
    if (p_screenshot_frame) {
        uint32 screenshot_frame =
            (uint32)strtoul(p_screenshot_frame, NULL, 10);
        if (screenshot_frame > 0) {
            g_app->screenshot_frame = screenshot_frame;
        }
    }
    Canvas_Theme_Resolve(
        &g_app->theme,
        Canvas_Theme_Mode_From_String(getenv("INFINITE_CANVAS_THEME")));

    SetConfigFlags(FLAG_WINDOW_RESIZABLE | FLAG_WINDOW_HIGHDPI | FLAG_MSAA_4X_HINT);
#if defined(INFINITE_CANVAS_DEV_UI)
    InitWindow(1920, 1200, "Infinite Canvas");
    SetWindowMinSize(1200, 800);
#else
    InitWindow(1280, 720, "Infinite Canvas");
    SetWindowMinSize(640, 360);
#endif
    SetTargetFPS(60);
#if !defined(PLATFORM_WEB)
    SetWindowFocused();
#endif
    if (!Canvas_Web_Surface_Init(
            &g_app->web_surface,
            p_arena,
            g_app->theme.resolved_mode == CANVAS_THEME_DARK ? TRUE : FALSE)) {
        TraceLog(LOG_WARNING, "Web surface initialization failed");
    }
    g_app->agent_initialized =
        Canvas_Agent_Service_Init(&g_app->agent_service, p_arena);
    if (!g_app->agent_initialized) {
        TraceLog(LOG_WARNING, "Agent service initialization failed");
    }

    g_app->font = LoadFontEx("infinite_canvas/assets/Inter-Variable.ttf", 48, NULL, 0);
    g_app->owns_font = g_app->font.texture.id != 0;
    if (!g_app->owns_font) g_app->font = GetFontDefault();
    else SetTextureFilter(g_app->font.texture, TEXTURE_FILTER_BILINEAR);
    Canvas_Camera_Init(&g_app->camera, GetScreenWidth(), GetScreenHeight());
    Canvas_Scene_Init(&g_app->scene, p_arena);
    if (getenv("INFINITE_CANVAS_BROWSER_STRESS")) {
        Canvas_Scene_Add_Browser_Grid(&g_app->scene);
    } else {
        Canvas_Scene_Add_Demo(&g_app->scene);
    }
    if (getenv("INFINITE_CANVAS_DICTATION_AUTOSTART")) {
        Canvas_App_Set_Dictation_Active(TRUE);
    }
    const char *p_initial_prompt = getenv("INFINITE_CANVAS_AGENT_PROMPT");
    if (p_initial_prompt && p_initial_prompt[0]) {
        Canvas_App_Submit_Prompt(p_initial_prompt);
    }
    const char *p_image_source = getenv("INFINITE_CANVAS_IMAGE_SOURCE");
    if (p_image_source) {
        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_IMAGE) {
                snprintf(p_entity->text, sizeof(p_entity->text), "%s", p_image_source);
                break;
            }
        }
    }
#if !defined(PLATFORM_WEB)
    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_WEB_CONTENT) {
            snprintf(p_entity->text, sizeof(p_entity->text), "https://www.google.com");
            break;
        }
    }
#endif
#if defined(INFINITE_CANVAS_DEV_UI)
    Canvas_Dev_UI_Init(&g_app->dev_ui, g_app->font, &g_app->theme);
#endif

#if defined(PLATFORM_WEB)
    emscripten_set_main_loop(Canvas_App_Draw_Frame, 0, 1);
#else
    while (!WindowShouldClose()) Canvas_App_Draw_Frame();
    if (g_app->agent_initialized) {
        Canvas_Agent_Service_Shutdown(&g_app->agent_service);
    }
    Canvas_Web_Surface_Shutdown(&g_app->web_surface);
    if (g_app->owns_font) UnloadFont(g_app->font);
    Dowa_Arena_Free(p_arena);
    g_app = NULL;
    CloseWindow();
#endif

    return 0;
}