view infinite_canvas/canvas_test.c @ 280:49e9e591c9bb

Add persistent dictation, prewarmed WebRTC speech input, Copilot SDK routing, animated conversation lifecycle controls, parking, and architecture coverage.
author MrJuneJune <me@mrjunejune.com>
date Tue, 18 Aug 2026 19:14:53 -0700
parents 8d560f50ed4c
children c57149ad216e
line wrap: on
line source

#include "infinite_canvas/canvas.h"

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

static boolean Near(float left, float right)
{
    return fabsf(left - right) < 0.001f ? TRUE : FALSE;
}

static Rectangle Entity_Bounds_For_Test(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),
            fabsf(p_entity->size.x),
            fabsf(p_entity->size.y),
        };
    }
    return (Rectangle){
        p_entity->position.x,
        p_entity->position.y,
        p_entity->size.x,
        p_entity->size.y,
    };
}

static void Test_Camera_Round_Trip(void)
{
    Canvas_Camera camera;
    Canvas_Camera_Init(&camera, 1200, 800);
    camera.target = (Vector2){340.0f, -125.0f};
    camera.zoom = 2.5f;

    Vector2 world = {-22.0f, 718.0f};
    Vector2 screen = Canvas_Camera_World_To_Screen(&camera, world);
    Vector2 result = Canvas_Camera_Screen_To_World(&camera, screen);
    assert(Near(result.x, world.x));
    assert(Near(result.y, world.y));
}

static void Test_Theme_Resolution(void)
{
    Canvas_Theme light;
    Canvas_Theme dark;
    Canvas_Theme_Resolve(&light, CANVAS_THEME_LIGHT);
    Canvas_Theme_Resolve(&dark, CANVAS_THEME_DARK);

    assert(light.resolved_mode == CANVAS_THEME_LIGHT);
    assert(light.background.r == 255);
    assert(light.toast_surface.r == 255);
    assert(dark.resolved_mode == CANVAS_THEME_DARK);
    assert(dark.background.r == 15);
    assert(dark.toast_surface.r == 24);
    assert(light.switch_track_on.g > light.switch_track_on.b);
    assert(dark.switch_track_on.g > dark.switch_track_on.b);
    assert(light.switch_track_on.r != light.accent.r);
    assert(dark.switch_track_on.r != dark.accent.r);
    assert(light.pin.r > light.pin.b);
    assert(dark.pin.r > dark.pin.b);
    assert(light.pin.r != light.accent.r);
    assert(dark.pin.r != dark.accent.r);
    assert(light.button.r != dark.button.r);
    assert(light.selection.r != dark.selection.r);
    assert(light.danger.r != dark.danger.r);
    assert(Canvas_Theme_Mode_From_String("light") == CANVAS_THEME_LIGHT);
    assert(Canvas_Theme_Mode_From_String("dark") == CANVAS_THEME_DARK);
    assert(Canvas_Theme_Mode_From_String("invalid") == CANVAS_THEME_AUTO);

    Canvas_Theme_Toggle(&light);
    assert(light.resolved_mode == CANVAS_THEME_DARK);
    assert(light.requested_mode == CANVAS_THEME_DARK);
    Canvas_Theme_Toggle(&light);
    assert(light.resolved_mode == CANVAS_THEME_LIGHT);
}

static void Test_Anchored_Zoom(void)
{
    Canvas_Camera camera;
    Canvas_Camera_Init(&camera, 1000, 600);
    camera.target = (Vector2){125.0f, -40.0f};
    Vector2 cursor = {770.0f, 180.0f};
    Vector2 before = Canvas_Camera_Screen_To_World(&camera, cursor);
    Canvas_Camera_Zoom_At(&camera, cursor, 3.0f);
    Vector2 after = Canvas_Camera_Screen_To_World(&camera, cursor);
    assert(Near(before.x, after.x));
    assert(Near(before.y, after.y));
}

static void Test_Viewport_Bounds(void)
{
    Canvas_Camera camera;
    Canvas_Camera_Init(&camera, 200, 100);
    camera.zoom = 2.0f;
    Rectangle bounds = Canvas_Camera_Viewport_Bounds(&camera);
    assert(Near(bounds.x, -50.0f));
    assert(Near(bounds.y, -25.0f));
    assert(Near(bounds.width, 100.0f));
    assert(Near(bounds.height, 50.0f));
}

static void Test_Grid_Spacing(void)
{
    assert(Near(Canvas_Grid_Spacing(1.0f), 64.0f));
    assert(Near(Canvas_Grid_Spacing(0.25f), 256.0f));
    assert(Near(Canvas_Grid_Spacing(4.0f), 16.0f));
}

static void Test_Entity_Picking(void)
{
    Dowa_Arena *p_arena = Dowa_Arena_Create(ONE_MEGA_BYTE);
    assert(p_arena);

    Canvas_Scene scene;
    Canvas_Scene_Init(&scene, p_arena);
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_RECTANGLE, (Vector2){10.0f, 20.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_CIRCLE, (Vector2){300.0f, 80.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_LINE, (Vector2){400.0f, 20.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_TEXT, (Vector2){650.0f, 40.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_BUTTON, (Vector2){850.0f, 40.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_TEXT_AREA, (Vector2){1050.0f, 40.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_DROPDOWN, (Vector2){1400.0f, 40.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_WEB_CONTENT, (Vector2){1700.0f, 40.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_ACCORDION, (Vector2){2500.0f, 40.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_CARD, (Vector2){2850.0f, 40.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_CALENDAR, (Vector2){3180.0f, 40.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_SWITCH, (Vector2){3510.0f, 40.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_TABLE, (Vector2){3650.0f, 40.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_NOTIFICATION, (Vector2){4120.0f, 40.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_SCROLL_AREA, (Vector2){4530.0f, 40.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_IMAGE, (Vector2){4900.0f, 40.0f}));

    assert(Canvas_Scene_Pick(&scene, (Vector2){30.0f, 40.0f}, 1.0f) == 0);
    assert(Canvas_Scene_Pick(&scene, (Vector2){300.0f, 80.0f}, 1.0f) == 1);
    assert(Canvas_Scene_Pick(&scene, (Vector2){490.0f, 60.0f}, 1.0f) == 2);
    assert(Canvas_Scene_Pick(&scene, (Vector2){700.0f, 55.0f}, 1.0f) == 3);
    assert(Canvas_Scene_Pick(&scene, (Vector2){900.0f, 60.0f}, 1.0f) == 4);
    assert(Canvas_Scene_Pick(&scene, (Vector2){1100.0f, 80.0f}, 1.0f) == 5);
    assert(Canvas_Scene_Pick(&scene, (Vector2){1450.0f, 60.0f}, 1.0f) == 6);
    scene.p_entities[6].active = TRUE;
    assert(Canvas_Scene_Pick(&scene, (Vector2){1450.0f, 120.0f}, 1.0f) == 6);
    assert(Canvas_Scene_Pick(&scene, (Vector2){1450.0f, 190.0f}, 1.0f) == 6);
    assert(Canvas_Scene_Pick(&scene, (Vector2){1704.0f, 44.0f}, 1.0f) == 7);
    assert(Canvas_Scene_Pick(&scene, (Vector2){1750.0f, 140.0f}, 1.0f) == -1);
    assert(Canvas_Scene_Pick(&scene, (Vector2){2550.0f, 60.0f}, 1.0f) == 8);
    assert(Canvas_Scene_Pick(&scene, (Vector2){2900.0f, 100.0f}, 1.0f) == 9);
    assert(Canvas_Scene_Pick(&scene, (Vector2){3230.0f, 100.0f}, 1.0f) == 10);
    assert(Canvas_Scene_Pick(&scene, (Vector2){3540.0f, 58.0f}, 1.0f) == 11);
    assert(Canvas_Scene_Pick(&scene, (Vector2){3700.0f, 100.0f}, 1.0f) == 12);
    assert(Canvas_Scene_Pick(&scene, (Vector2){4170.0f, 80.0f}, 1.0f) == 13);
    assert(Canvas_Scene_Pick(&scene, (Vector2){4580.0f, 100.0f}, 1.0f) == 14);
    assert(Canvas_Scene_Pick(&scene, (Vector2){4950.0f, 100.0f}, 1.0f) == 15);
    assert(Canvas_Scene_Pick(&scene, (Vector2){5500.0f, 500.0f}, 1.0f) == -1);

    Dowa_Arena_Free(p_arena);
}

static void Test_Overlapping_Entities_Use_Topmost_Z_Order(void)
{
    Dowa_Arena *p_arena = Dowa_Arena_Create(4 * ONE_MEGA_BYTE);
    assert(p_arena);

    Canvas_Scene scene;
    Canvas_Scene_Init(&scene, p_arena);
    Vector2 position = {0.0f, 0.0f};
    Vector2 pointer = {40.0f, 30.0f};
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_BUTTON, position));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_BUTTON, position));
    assert(Canvas_Scene_Pick(&scene, pointer, 1.0f) == 1);
    assert(Canvas_Scene_Topmost_Visual(&scene, pointer, 1.0f) == 1);

    scene.p_entities[1].removing = TRUE;
    assert(Canvas_Scene_Pick(&scene, pointer, 1.0f) == 0);
    assert(Canvas_Scene_Topmost_Visual(&scene, pointer, 1.0f) == 0);

    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_WEB_CONTENT, position));
    assert(Canvas_Scene_Topmost_Visual(&scene, pointer, 1.0f) == 2);
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_BUTTON, position));
    assert(Canvas_Scene_Pick(&scene, pointer, 1.0f) == 3);
    assert(Canvas_Scene_Topmost_Visual(&scene, pointer, 1.0f) == 3);

    Dowa_Arena_Free(p_arena);
}

static void Test_New_Primitive_Defaults(void)
{
    Dowa_Arena *p_arena = Dowa_Arena_Create(ONE_MEGA_BYTE);
    assert(p_arena);

    Canvas_Scene scene;
    Canvas_Scene_Init(&scene, p_arena);
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_ACCORDION, (Vector2){0.0f, 0.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_CARD, (Vector2){0.0f, 0.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_CALENDAR, (Vector2){0.0f, 0.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_SWITCH, (Vector2){0.0f, 0.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_TABLE, (Vector2){0.0f, 0.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_NOTIFICATION, (Vector2){0.0f, 0.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_SCROLL_AREA, (Vector2){0.0f, 0.0f}));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_IMAGE, (Vector2){0.0f, 0.0f}));

    assert(Near(scene.p_entities[0].size.x, 300.0f));
    assert(Near(scene.p_entities[0].size.y, 56.0f));
    assert(Near(scene.p_entities[1].size.x, 280.0f));
    assert(Near(scene.p_entities[1].size.y, 220.0f));
    assert(scene.p_entities[2].value == 17);
    assert(Near(scene.p_entities[3].size.x, 64.0f));
    assert(Near(scene.p_entities[3].size.y, 36.0f));
    assert(Near(scene.p_entities[4].size.x, 420.0f));
    assert(scene.p_entities[4].value == 1);
    assert(scene.p_entities[5].active);
    assert(Near(scene.p_entities[6].size.y, 220.0f));
    assert(strstr(scene.p_entities[7].text, "image-placeholder.svg"));
    assert(strcmp(Canvas_Entity_Type_Name(CANVAS_ENTITY_ACCORDION), "Accordion") == 0);
    assert(strcmp(Canvas_Entity_Type_Name(CANVAS_ENTITY_CARD), "Card") == 0);
    assert(strcmp(Canvas_Entity_Type_Name(CANVAS_ENTITY_CALENDAR), "Calendar") == 0);
    assert(strcmp(Canvas_Entity_Type_Name(CANVAS_ENTITY_SWITCH), "Switch") == 0);
    assert(strcmp(Canvas_Entity_Type_Name(CANVAS_ENTITY_TABLE), "Table") == 0);
    assert(strcmp(Canvas_Entity_Type_Name(CANVAS_ENTITY_NOTIFICATION), "Notification") == 0);
    assert(strcmp(Canvas_Entity_Type_Name(CANVAS_ENTITY_SCROLL_AREA), "Scrollable area") == 0);
    assert(strcmp(Canvas_Entity_Type_Name(CANVAS_ENTITY_IMAGE), "Image") == 0);

    Dowa_Arena_Free(p_arena);
}

static void Test_Text_Editing_Focus(void)
{
    Dowa_Arena *p_arena = Dowa_Arena_Create(ONE_MEGA_BYTE);
    assert(p_arena);

    Canvas_Scene scene;
    Canvas_Scene_Init(&scene, p_arena);
    assert(!Canvas_Scene_Is_Text_Editing(&scene));
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_TEXT_AREA, (Vector2){0.0f, 0.0f}));
    assert(sizeof(scene.p_entities[0].text) == CANVAS_ENTITY_TEXT_CAPACITY);
    assert(scene.p_entities[0].text_cursor == 0);
    assert(scene.p_entities[0].text_selection_anchor == 0);

    scene.selected_index = 0;
    assert(!Canvas_Scene_Is_Text_Editing(&scene));
    scene.p_entities[0].active = TRUE;
    assert(Canvas_Scene_Is_Text_Editing(&scene));
    scene.p_entities[0].active = FALSE;
    assert(!Canvas_Scene_Is_Text_Editing(&scene));

    Dowa_Arena_Free(p_arena);
}

static void Test_Entity_Ids_Remain_Unique_After_Clear(void)
{
    Dowa_Arena *p_arena = Dowa_Arena_Create(ONE_MEGA_BYTE);
    assert(p_arena);

    Canvas_Scene scene;
    Canvas_Scene_Init(&scene, p_arena);
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_WEB_CONTENT, (Vector2){0.0f, 0.0f}));
    uint32 first_id = scene.p_entities[0].id;
    Canvas_Scene_Clear(&scene);
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_WEB_CONTENT, (Vector2){0.0f, 0.0f}));
    assert(scene.p_entities[0].id != first_id);

    Dowa_Arena_Free(p_arena);
}

static void Test_Entity_Fade_Lifecycle_Commands(void)
{
    Dowa_Arena *p_arena = Dowa_Arena_Create(4 * ONE_MEGA_BYTE);
    assert(p_arena);

    Canvas_Scene scene;
    Canvas_Scene_Init(&scene, p_arena);
    assert(Canvas_Scene_Add(
        &scene,
        CANVAS_ENTITY_RECTANGLE,
        (Vector2){0.0f, 0.0f}));
    assert(Canvas_Scene_Add(
        &scene,
        CANVAS_ENTITY_TEXT,
        (Vector2){200.0f, 0.0f}));
    assert(Near(scene.p_entities[0].visibility_amount, 0.0f));
    assert(!scene.p_entities[0].removing);

    scene.selected_index = 0;
    Canvas_Scene_Fade_Out_Selected(&scene);
    assert(scene.p_entities[0].removing);
    assert(scene.selected_index == -1);
    assert(!scene.p_entities[1].removing);

    Canvas_Scene_Fade_Out_All(&scene);
    assert(scene.p_entities[0].removing);
    assert(scene.p_entities[1].removing);

    Dowa_Arena_Free(p_arena);
}

static void Test_Browser_Grid(void)
{
    Dowa_Arena *p_arena = Dowa_Arena_Create(ONE_MEGA_BYTE);
    assert(p_arena);

    Canvas_Scene scene;
    Canvas_Scene_Init(&scene, p_arena);
    Canvas_Scene_Add_Browser_Grid(&scene);
    assert(Dowa_Array_Length(scene.p_entities) == 10);
    for (size_t index = 0; index < Dowa_Array_Length(scene.p_entities); index++) {
        assert(scene.p_entities[index].type == CANVAS_ENTITY_WEB_CONTENT);
        assert(Near(scene.p_entities[index].size.x, 236.0f));
        assert(Near(scene.p_entities[index].size.y, 176.0f));
    }

    Dowa_Arena_Free(p_arena);
}

static void Test_Lucide_Gallery_Reuses_Entity_And_Fits_Camera(void)
{
    Dowa_Arena *p_arena = Dowa_Arena_Create(ONE_MEGA_BYTE);
    assert(p_arena);

    Canvas_Camera camera;
    Canvas_Camera_Init(&camera, 1920, 1200);
    Canvas_Scene scene;
    Canvas_Scene_Init(&scene, p_arena);

    assert(Canvas_Scene_Show_Lucide_Gallery(&scene, &camera));
    assert(Dowa_Array_Length(scene.p_entities) == 1);
    assert(scene.selected_index == 0);
    Canvas_Entity *p_gallery = &scene.p_entities[0];
    assert(p_gallery->type == CANVAS_ENTITY_LUCIDE_GALLERY);
    assert(strcmp(p_gallery->text, "") == 0);
    assert(Near(p_gallery->size.x, 1440.0f));
    assert(Near(p_gallery->size.y, 1040.0f));

    Vector2 screen_min =
        Canvas_Camera_World_To_Screen(&camera, p_gallery->position);
    Vector2 screen_max = Canvas_Camera_World_To_Screen(
        &camera,
        (Vector2){
            p_gallery->position.x + p_gallery->size.x,
            p_gallery->position.y + p_gallery->size.y,
        });
    assert(screen_min.x >= 319.0f);
    assert(screen_min.y >= 55.0f);
    assert(screen_max.x <= 1865.0f);
    assert(screen_max.y <= 1145.0f);

    assert(Canvas_Scene_Show_Lucide_Gallery(&scene, &camera));
    assert(Dowa_Array_Length(scene.p_entities) == 1);

    Dowa_Arena_Free(p_arena);
}

static void Test_Lucide_Search_Is_Case_Insensitive(void)
{
    assert(Canvas_Lucide_Count_Matches("") == 1767);
    assert(Canvas_Lucide_Count_Matches("pin") > 0);
    assert(Canvas_Lucide_Count_Matches("PIN") ==
        Canvas_Lucide_Count_Matches("pin"));
    assert(Canvas_Lucide_Count_Matches("not-a-real-lucide-icon") == 0);
}

static void Test_Demo_Contains_Every_Entity_Type(void)
{
    Dowa_Arena *p_arena = Dowa_Arena_Create(ONE_MEGA_BYTE);
    assert(p_arena);

    Canvas_Scene scene;
    Canvas_Scene_Init(&scene, p_arena);
    Canvas_Scene_Add_Demo(&scene);
    assert(Dowa_Array_Length(scene.p_entities) == CANVAS_ENTITY_TYPE_COUNT + 1);

    int32 counts[CANVAS_ENTITY_TYPE_COUNT] = {0};
    for (size_t index = 0; index < Dowa_Array_Length(scene.p_entities); index++) {
        counts[scene.p_entities[index].type]++;
    }
    for (int32 type = 0; type < CANVAS_ENTITY_TYPE_COUNT; type++) {
        if (type == CANVAS_ENTITY_LUCIDE_GALLERY) {
            assert(counts[type] == 0);
            continue;
        }
        int32 expected = type == CANVAS_ENTITY_NOTIFICATION ? 3 : 1;
        assert(counts[type] == expected);
    }
    for (size_t index = 0; index < Dowa_Array_Length(scene.p_entities); index++) {
        if (scene.p_entities[index].type == CANVAS_ENTITY_NOTIFICATION) continue;
        Rectangle bounds = Entity_Bounds_For_Test(&scene.p_entities[index]);
        assert(bounds.x >= -600.0f);
        assert(bounds.y >= -600.0f);
        assert(bounds.x + bounds.width <= 960.0f);
        assert(bounds.y + bounds.height <= 600.0f);
        for (size_t other = index + 1;
             other < Dowa_Array_Length(scene.p_entities);
             other++) {
            if (scene.p_entities[other].type == CANVAS_ENTITY_NOTIFICATION) continue;
            Rectangle other_bounds = Entity_Bounds_For_Test(&scene.p_entities[other]);
            assert(!CheckCollisionRecs(bounds, other_bounds));
        }
    }

    Dowa_Arena_Free(p_arena);
}

static void Test_Pinned_Entity_Stays_In_Screen_Space(void)
{
    Dowa_Arena *p_arena = Dowa_Arena_Create(ONE_MEGA_BYTE);
    assert(p_arena);

    Canvas_Camera camera;
    Canvas_Camera_Init(&camera, 1000, 600);
    Canvas_Scene scene;
    Canvas_Scene_Init(&scene, p_arena);
    assert(Canvas_Scene_Add(&scene, CANVAS_ENTITY_RECTANGLE, (Vector2){80.0f, 40.0f}));
    scene.selected_index = 0;
    Canvas_Scene_Toggle_Selected_Pin(&scene, &camera);
    assert(scene.p_entities[0].pinned);

    Vector2 original_screen =
        Canvas_Camera_World_To_Screen(&camera, scene.p_entities[0].position);
    Vector2 original_size = {
        scene.p_entities[0].size.x * camera.zoom,
        scene.p_entities[0].size.y * camera.zoom,
    };
    camera.target = (Vector2){900.0f, -500.0f};
    camera.zoom = 3.0f;
    Canvas_Scene_Sync_Pinned(&scene, &camera);

    Vector2 pinned_screen =
        Canvas_Camera_World_To_Screen(&camera, scene.p_entities[0].position);
    assert(Near(pinned_screen.x, original_screen.x));
    assert(Near(pinned_screen.y, original_screen.y));
    assert(Near(scene.p_entities[0].size.x * camera.zoom, original_size.x));
    assert(Near(scene.p_entities[0].size.y * camera.zoom, original_size.y));

    Canvas_Scene_Toggle_Selected_Pin(&scene, &camera);
    assert(!scene.p_entities[0].pinned);
    Dowa_Arena_Free(p_arena);
}

static void Test_Visible_Context_Tracks_Camera_And_Values(void)
{
    Dowa_Arena *p_arena = Dowa_Arena_Create(ONE_MEGA_BYTE);
    assert(p_arena);

    Canvas_Camera camera;
    Canvas_Camera_Init(&camera, 200, 100);
    Canvas_Scene scene;
    Canvas_Scene_Init(&scene, p_arena);
    assert(Canvas_Scene_Add(
        &scene,
        CANVAS_ENTITY_TEXT_AREA,
        (Vector2){-80.0f, -30.0f}));
    assert(Canvas_Scene_Add(
        &scene,
        CANVAS_ENTITY_DROPDOWN,
        (Vector2){250.0f, -20.0f}));
    assert(Canvas_Scene_Add(
        &scene,
        CANVAS_ENTITY_SWITCH,
        (Vector2){700.0f, 0.0f}));

    snprintf(scene.p_entities[0].text, sizeof(scene.p_entities[0].text), "Visible note");
    scene.p_entities[0].active = TRUE;
    scene.p_entities[0].text_cursor = 7;
    scene.p_entities[0].text_selection_anchor = 2;
    scene.p_entities[1].value = 2;
    scene.p_entities[1].active = TRUE;
    scene.p_entities[2].active = TRUE;

    char context[4096];
    Canvas_Context_Snapshot snapshot = Canvas_Scene_Build_Visible_Context(
        &scene,
        &camera,
        context,
        sizeof(context));
    assert(snapshot.visible_count == 1);
    assert(!snapshot.truncated);
    assert(strstr(context, "Visible note"));
    assert(strstr(context, "editing=true"));
    assert(strstr(context, "cursor=7"));
    assert(strstr(context, "selection_anchor=2"));
    assert(!strstr(context, "Research"));
    assert(!strstr(context, "checked=true"));

    camera.zoom = 0.25f;
    snapshot = Canvas_Scene_Build_Visible_Context(
        &scene,
        &camera,
        context,
        sizeof(context));
    assert(snapshot.visible_count == 2);
    assert(strstr(context, "selected=\"Research\""));
    assert(strstr(context, "open=true"));
    assert(!strstr(context, "checked=true"));

    camera.target.x = 900.0f;
    snapshot = Canvas_Scene_Build_Visible_Context(
        &scene,
        &camera,
        context,
        sizeof(context));
    assert(snapshot.visible_count == 1);
    assert(strstr(context, "checked=true"));
    assert(!strstr(context, "Visible note"));

    char small_context[24];
    snapshot = Canvas_Scene_Build_Visible_Context(
        &scene,
        &camera,
        small_context,
        sizeof(small_context));
    assert(snapshot.visible_count == 1);
    assert(snapshot.truncated);
    assert(small_context[sizeof(small_context) - 1] == '\0');

    Dowa_Arena_Free(p_arena);
}

static void Test_Dictation_Scratchpad_Is_Not_A_Routing_Target(void)
{
    Dowa_Arena *p_arena = Dowa_Arena_Create(ONE_MEGA_BYTE);
    assert(p_arena);

    Canvas_Camera camera;
    Canvas_Camera_Init(&camera, 800, 600);
    Canvas_Scene scene;
    Canvas_Scene_Init(&scene, p_arena);
    assert(Canvas_Scene_Add(
        &scene,
        CANVAS_ENTITY_TEXT_AREA,
        (Vector2){-260.0f, -120.0f}));
    snprintf(
        scene.p_entities[0].label,
        sizeof(scene.p_entities[0].label),
        "Dictation");
    snprintf(
        scene.p_entities[0].text,
        sizeof(scene.p_entities[0].text),
        "Route this thought");
    uint32 conversation_id = Canvas_Scene_Add_Conversation(
        &scene,
        "Visible session",
        "Earlier thought",
        "Earlier response",
        (Vector2){40.0f, -120.0f});
    assert(conversation_id != 0);

    char context[4096];
    Canvas_Context_Snapshot snapshot = Canvas_Scene_Build_Visible_Context(
        &scene,
        &camera,
        context,
        sizeof(context));
    assert(snapshot.visible_count == 1);
    assert(!strstr(context, "Route this thought"));
    assert(strstr(context, "Visible session"));
    assert(strstr(context, TextFormat("id=%u", conversation_id)));

    Dowa_Arena_Free(p_arena);
}

static void Test_Conversation_Create_And_Append(void)
{
    Dowa_Arena *p_arena = Dowa_Arena_Create(ONE_MEGA_BYTE);
    assert(p_arena);
    Canvas_Scene scene;
    Canvas_Scene_Init(&scene, p_arena);

    uint32 id = Canvas_Scene_Add_Conversation(
        &scene,
        "Rendering architecture",
        "How should images render?",
        "Use a rich entity with typed resources.",
        (Vector2){10.0f, 20.0f});
    assert(id != 0);
    assert(Dowa_Array_Length(scene.p_entities) == 1);
    assert(scene.p_entities[0].type == CANVAS_ENTITY_CONVERSATION);
    assert(strcmp(scene.p_entities[0].label, "Rendering architecture") == 0);
    assert(strstr(scene.p_entities[0].text, "How should images render?"));

    assert(Canvas_Scene_Append_Conversation(
        &scene,
        id,
        "What about browser content?",
        "Keep the resource typed and composited by its backend."));
    assert(strstr(scene.p_entities[0].text, "What about browser content?"));
    assert(strstr(scene.p_entities[0].text, "composited by its backend"));
    assert(!Canvas_Scene_Append_Conversation(
        &scene,
        id + 1,
        "Missing",
        "Missing"));
    Dowa_Arena_Free(p_arena);
}

static void Test_Conversation_Collapse_And_Parking(void)
{
    Dowa_Arena *p_arena = Dowa_Arena_Create(ONE_MEGA_BYTE);
    assert(p_arena);
    Canvas_Camera camera;
    Canvas_Camera_Init(&camera, 800, 600);
    Canvas_Scene scene;
    Canvas_Scene_Init(&scene, p_arena);

    Vector2 original = {20.0f, 30.0f};
    uint32 id = Canvas_Scene_Add_Conversation(
        &scene,
        "Parkable session",
        "Question",
        "Answer",
        original);
    Canvas_Entity *p_conversation = &scene.p_entities[0];
    assert(Near(p_conversation->animation_amount, 1.0f));
    Canvas_Conversation_Set_Collapsed(p_conversation, TRUE);
    assert(p_conversation->active);
    Canvas_Conversation_Set_Collapsed(p_conversation, FALSE);
    assert(!p_conversation->active);

    assert(Canvas_Scene_Set_Conversation_Parked(&scene, id, TRUE));
    assert(p_conversation->parked);
    assert(Canvas_Scene_Parked_Conversation_Count(&scene) == 1);
    assert(!Near(p_conversation->position.x, original.x));
    char context[4096];
    Canvas_Context_Snapshot snapshot = Canvas_Scene_Build_Visible_Context(
        &scene,
        &camera,
        context,
        sizeof(context));
    assert(snapshot.visible_count == 0);
    assert(Canvas_Scene_Show_Parking_Lot(&scene, &camera));
    snapshot = Canvas_Scene_Build_Visible_Context(
        &scene,
        &camera,
        context,
        sizeof(context));
    assert(snapshot.visible_count == 0);

    assert(Canvas_Scene_Set_Conversation_Parked(&scene, id, FALSE));
    assert(!p_conversation->parked);
    assert(Canvas_Scene_Parked_Conversation_Count(&scene) == 0);
    assert(Near(p_conversation->position.x, original.x));
    assert(Near(p_conversation->position.y, original.y));
    Dowa_Arena_Free(p_arena);
}

int main(void)
{
    Test_Theme_Resolution();
    Test_Camera_Round_Trip();
    Test_Anchored_Zoom();
    Test_Viewport_Bounds();
    Test_Grid_Spacing();
    Test_Entity_Picking();
    Test_Overlapping_Entities_Use_Topmost_Z_Order();
    Test_New_Primitive_Defaults();
    Test_Text_Editing_Focus();
    Test_Entity_Ids_Remain_Unique_After_Clear();
    Test_Entity_Fade_Lifecycle_Commands();
    Test_Browser_Grid();
    Test_Lucide_Gallery_Reuses_Entity_And_Fits_Camera();
    Test_Lucide_Search_Is_Case_Insensitive();
    Test_Demo_Contains_Every_Entity_Type();
    Test_Conversation_Create_And_Append();
    Test_Pinned_Entity_Stays_In_Screen_Space();
    Test_Visible_Context_Tracks_Camera_And_Values();
    Test_Dictation_Scratchpad_Is_Not_A_Routing_Target();
    Test_Conversation_Collapse_And_Parking();
    printf("Infinite canvas tests passed.\n");
    return 0;
}