view infinite_canvas/main.c @ 278:8d560f50ed4c

Improve infinite canvas interactions and browser chrome Render Lucide icons directly with Raylib, add searchable icon browsing, robust text editing, entity lifecycle animations, z-order-safe input, semantic themes, and animated editable browser controls. Document rendering, pinning, context, and component extension for future agents. Co-authored-by: Copilot <[email protected]> Copilot-Session: f68442b1-fa8f-46a0-9689-81710613bbd4
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 22:16:14 -0700
parents b55c22cff335
children 49e9e591c9bb
line wrap: on
line source

#include "infinite_canvas/canvas.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 <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;
    Font font;
    Canvas_Theme theme;
    boolean owns_font;
    const char *p_screenshot_path;
    uint32 frame_count;
#if defined(INFINITE_CANVAS_DEV_UI)
    Canvas_Dev_UI dev_ui;
#endif
} Canvas_App;

static Canvas_App *g_app = NULL;

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;
#if defined(INFINITE_CANVAS_DEV_UI)
    block_pointer_input = Canvas_Dev_UI_Contains_Pointer(&g_app->dev_ui);
#endif
    Canvas_Web_Surface_Set_Dark_Mode(
        &g_app->web_surface,
        g_app->theme.resolved_mode == CANVAS_THEME_DARK ? TRUE : FALSE);
    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 && 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) ||
        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 &&
        (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);

    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);
#if defined(INFINITE_CANVAS_DEV_UI)
    Canvas_Dev_UI_Draw(
        &g_app->dev_ui,
        &g_app->camera,
        &g_app->scene,
        g_app->font,
        &g_app->theme);
#endif
    EndDrawing();
    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 == 180) {
        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;
    g_app->p_screenshot_path = getenv("INFINITE_CANVAS_SCREENSHOT");
    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->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);
    }
    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();
    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;
}