view infinite_canvas/dev_ui.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/dev_ui.h"

#include <math.h>

#define RAYGUI_IMPLEMENTATION
#include "third_party/raylib/include/raygui.h"

static const Rectangle DEV_PANEL = {18.0f, 18.0f, 264.0f, 478.0f};

static Rectangle Canvas_Dev_UI_Context_Bounds(void)
{
    float width = 610.0f;
    float x = (float)GetScreenWidth() - width - 18.0f;
    if (x < DEV_PANEL.x + DEV_PANEL.width + 18.0f) {
        x = DEV_PANEL.x + DEV_PANEL.width + 18.0f;
        width = (float)GetScreenWidth() - x - 18.0f;
    }
    return (Rectangle){
        x,
        18.0f,
        width,
        (float)GetScreenHeight() - 36.0f,
    };
}

static float Canvas_Dev_UI_Draw_Wrapped_Context(
    const char *p_text,
    Font font,
    Rectangle bounds,
    float scroll,
    Color color)
{
    const float font_size = 13.0f;
    const float line_height = 19.0f;
    const float spacing = 0.0f;
    char line[256] = {0};
    size_t line_length = 0;
    float y = bounds.y - scroll;

    for (const char *p_cursor = p_text;; p_cursor++) {
        boolean at_end = *p_cursor == '\0';
        boolean at_newline = *p_cursor == '\n';
        if (!at_end && !at_newline) {
            line[line_length++] = *p_cursor;
            line[line_length] = '\0';
            if (line_length + 1 < sizeof(line) &&
                MeasureTextEx(font, line, font_size, spacing).x <= bounds.width) {
                continue;
            }
            if (line_length > 1) {
                char overflow = line[--line_length];
                line[line_length] = '\0';
                if (y + line_height >= bounds.y &&
                    y <= bounds.y + bounds.height) {
                    DrawTextEx(
                        font,
                        line,
                        (Vector2){bounds.x, y},
                        font_size,
                        spacing,
                        color);
                }
                y += line_height;
                line[0] = overflow;
                line[1] = '\0';
                line_length = 1;
            }
            continue;
        }

        if (line_length > 0 &&
            y + line_height >= bounds.y &&
            y <= bounds.y + bounds.height) {
            DrawTextEx(
                font,
                line,
                (Vector2){bounds.x, y},
                font_size,
                spacing,
                color);
        }
        y += line_height;
        line_length = 0;
        line[0] = '\0';
        if (at_end) break;
    }
    return y - bounds.y + scroll;
}

static void Canvas_Dev_UI_Draw_Context_Inspector(
    Canvas_Dev_UI *p_ui,
    const Canvas_Context_Snapshot *p_snapshot,
    Font font,
    const Canvas_Theme *p_theme)
{
    Rectangle panel = Canvas_Dev_UI_Context_Bounds();
    Canvas_Theme_Draw_Shadow(p_theme, panel, 0.04f, 14, 1.0f, 1.0f);
    DrawRectangleRounded(panel, 0.04f, 14, p_theme->surface);
    DrawRectangleRoundedLinesEx(panel, 0.04f, 14, 1.0f, p_theme->border);

    DrawTextEx(
        font,
        "Camera context",
        (Vector2){panel.x + 20.0f, panel.y + 18.0f},
        21.0f,
        0.0f,
        p_theme->text);
    DrawTextEx(
        font,
        TextFormat(
            "%d visible entities%s",
            (int)p_snapshot->visible_count,
            p_snapshot->truncated ? " - output truncated" : ""),
        (Vector2){panel.x + 20.0f, panel.y + 48.0f},
        13.0f,
        0.0f,
        p_snapshot->truncated ? p_theme->danger : p_theme->text_muted);

    Rectangle close = {
        panel.x + panel.width - 76.0f,
        panel.y + 16.0f,
        56.0f,
        30.0f,
    };
    if (GuiButtonRounded(close, "Close", 0.30f, 10)) {
        p_ui->context_open = FALSE;
        return;
    }

    Rectangle content = {
        panel.x + 20.0f,
        panel.y + 82.0f,
        panel.width - 40.0f,
        panel.height - 102.0f,
    };
    float max_scroll = fmaxf(0.0f, p_ui->context_content_height - content.height);
    if (CheckCollisionPointRec(GetMousePosition(), panel)) {
        p_ui->context_scroll -= GetMouseWheelMove() * 42.0f;
    }
    p_ui->context_scroll = fmaxf(0.0f, fminf(p_ui->context_scroll, max_scroll));

    BeginScissorMode(
        (int32)content.x,
        (int32)content.y,
        (int32)content.width,
        (int32)content.height);
    p_ui->context_content_height = Canvas_Dev_UI_Draw_Wrapped_Context(
        p_ui->context_buffer,
        font,
        content,
        p_ui->context_scroll,
        p_theme->text);
    EndScissorMode();

    max_scroll = fmaxf(0.0f, p_ui->context_content_height - content.height);
    if (max_scroll > 0.0f) {
        float track_height = content.height;
        float thumb_height = fmaxf(
            36.0f,
            track_height * content.height / p_ui->context_content_height);
        float thumb_y = content.y +
            (p_ui->context_scroll / max_scroll) * (track_height - thumb_height);
        DrawRectangleRounded(
            (Rectangle){
                panel.x + panel.width - 10.0f,
                thumb_y,
                4.0f,
                thumb_height,
            },
            0.5f,
            6,
            p_theme->text_muted);
    }
}

static void Canvas_Dev_UI_Set_Style(
    Font font,
    const Canvas_Theme *p_theme)
{
    GuiSetFont(font);
    GuiSetStyle(DEFAULT, TEXT_SIZE, 14);
    GuiSetStyle(DEFAULT, TEXT_SPACING, 0);
    GuiSetStyle(DEFAULT, BORDER_WIDTH, 1);
    GuiSetStyle(DEFAULT, BACKGROUND_COLOR, ColorToInt(p_theme->surface));

    int32 controls[] = {BUTTON, DROPDOWNBOX};
    for (size_t index = 0; index < sizeof(controls) / sizeof(controls[0]); index++) {
        int32 control = controls[index];
        GuiSetStyle(control, BORDER_WIDTH, 1);
        GuiSetStyle(control, BORDER_COLOR_NORMAL, ColorToInt(p_theme->border));
        GuiSetStyle(control, BASE_COLOR_NORMAL, ColorToInt(p_theme->surface_muted));
        GuiSetStyle(control, TEXT_COLOR_NORMAL, ColorToInt(p_theme->text));
        GuiSetStyle(control, BORDER_COLOR_FOCUSED, ColorToInt(p_theme->accent));
        GuiSetStyle(control, BASE_COLOR_FOCUSED, ColorToInt(p_theme->accent_soft));
        GuiSetStyle(control, TEXT_COLOR_FOCUSED, ColorToInt(p_theme->text));
        GuiSetStyle(control, BORDER_COLOR_PRESSED, ColorToInt(p_theme->accent));
        GuiSetStyle(control, BASE_COLOR_PRESSED, ColorToInt(p_theme->accent_soft));
        GuiSetStyle(control, TEXT_COLOR_PRESSED, ColorToInt(p_theme->accent));
    }
}

void Canvas_Dev_UI_Init(
    Canvas_Dev_UI *p_ui,
    Font font,
    const Canvas_Theme *p_theme)
{
    *p_ui = (Canvas_Dev_UI){
        .selected_type = CANVAS_ENTITY_RECTANGLE,
        .dropdown_open = FALSE,
        .context_open = FALSE,
    };
    Canvas_Dev_UI_Set_Style(font, p_theme);
}

boolean Canvas_Dev_UI_Contains_Pointer(const Canvas_Dev_UI *p_ui)
{
    // The expanded dropdown is modal: it draws outside DEV_PANEL and also
    // consumes outside clicks to close itself.
    if (p_ui->dropdown_open) return TRUE;

    Vector2 pointer = GetMousePosition();
    if (CheckCollisionPointRec(pointer, DEV_PANEL)) return TRUE;
    if (p_ui->context_open &&
        CheckCollisionPointRec(pointer, Canvas_Dev_UI_Context_Bounds())) {
        return TRUE;
    }
    return FALSE;
}

void Canvas_Dev_UI_Draw(
    Canvas_Dev_UI *p_ui,
    Canvas_Camera *p_camera,
    Canvas_Scene *p_scene,
    Font font,
    Canvas_Theme *p_theme)
{
    Canvas_Context_Snapshot context = Canvas_Scene_Build_Visible_Context(
        p_scene,
        p_camera,
        p_ui->context_buffer,
        sizeof(p_ui->context_buffer));

    Canvas_Theme_Draw_Shadow(p_theme, DEV_PANEL, 0.09f, 16, 1.0f, 1.0f);
    DrawRectangleRounded(DEV_PANEL, 0.09f, 16, p_theme->surface);
    DrawRectangleRoundedLinesEx(
        DEV_PANEL,
        0.09f,
        16,
        1.0f,
        p_theme->border);
    DrawTextEx(font, "Canvas", (Vector2){34.0f, 34.0f}, 21.0f, 0.0f, p_theme->text);
    DrawTextEx(
        font,
        "Developer controls",
        (Vector2){34.0f, 61.0f},
        12.0f,
        0.1f,
        p_theme->text_muted);

    if (p_ui->dropdown_open) GuiLock();

    if (GuiButtonRounded((Rectangle){34.0f, 126.0f, 112.0f, 34.0f}, "Add entity", 0.32f, 10)) {
        Canvas_Scene_Add(
            p_scene,
            (Canvas_Entity_Type)p_ui->selected_type,
            p_camera->target);
    }
    if (GuiButtonRounded((Rectangle){154.0f, 126.0f, 112.0f, 34.0f}, "Clear", 0.32f, 10)) {
        Canvas_Scene_Fade_Out_All(p_scene);
    }
    if (GuiButtonRounded((Rectangle){34.0f, 168.0f, 72.0f, 34.0f}, "Demo", 0.32f, 10)) {
        Canvas_Scene_Add_Demo(p_scene);
    }
    if (GuiButtonRounded((Rectangle){114.0f, 168.0f, 88.0f, 34.0f}, "Reset", 0.32f, 10)) {
        p_camera->target = (Vector2){0.0f, 0.0f};
        p_camera->zoom = 1.0f;
    }
    if (GuiButtonRounded(
            (Rectangle){210.0f, 168.0f, 56.0f, 34.0f},
            p_scene->show_grid ? "Grid" : "White",
            0.32f,
            10)) {
        p_scene->show_grid = !p_scene->show_grid;
    }
    if (GuiButtonRounded(
            (Rectangle){34.0f, 210.0f, 232.0f, 34.0f},
            "10 browser stress scene",
            0.32f,
            10)) {
        Canvas_Scene_Add_Browser_Grid(p_scene);
        p_camera->target = (Vector2){0.0f, 0.0f};
        p_camera->zoom = 1.0f;
    }

    const char *p_theme_label =
        p_theme->resolved_mode == CANVAS_THEME_DARK ?
            "Switch to light mode" :
            "Switch to dark mode";
    if (GuiButtonRounded(
            (Rectangle){34.0f, 252.0f, 232.0f, 34.0f},
            p_theme_label,
            0.32f,
            10)) {
        Canvas_Theme_Toggle(p_theme);
        Canvas_Dev_UI_Set_Style(font, p_theme);
    }

    if (GuiButtonRounded(
            (Rectangle){34.0f, 294.0f, 232.0f, 34.0f},
            TextFormat("View camera context (%d)", (int)context.visible_count),
            0.32f,
            10)) {
        p_ui->context_open = !p_ui->context_open;
        p_ui->context_scroll = 0.0f;
    }

    if (GuiButtonRounded(
            (Rectangle){34.0f, 336.0f, 232.0f, 34.0f},
            "Lucide icon gallery",
            0.32f,
            10)) {
        Canvas_Scene_Show_Lucide_Gallery(p_scene, p_camera);
    }

    DrawTextEx(font, TextFormat("%d FPS", GetFPS()), (Vector2){34.0f, 396.0f}, 12.0f, 0.0f, p_theme->text_muted);
    DrawTextEx(font, TextFormat("%.2fx zoom", p_camera->zoom), (Vector2){104.0f, 396.0f}, 12.0f, 0.0f, p_theme->text_muted);
    DrawTextEx(
        font,
        TextFormat("%d objects", (int)Dowa_Array_Length(p_scene->p_entities)),
        (Vector2){196.0f, 396.0f},
        12.0f,
        0.0f,
        p_theme->text_muted);
    DrawTextEx(
        font,
        "Drag objects directly on the canvas",
        (Vector2){34.0f, 442.0f},
        12.0f,
        0.0f,
        p_theme->text_muted);

    if (p_ui->context_open) {
        Canvas_Dev_UI_Draw_Context_Inspector(
            p_ui,
            &context,
            font,
            p_theme);
    }

    // Draw last so the expanded list overlays the other developer controls.
    if (p_ui->dropdown_open) GuiUnlock();
    Rectangle dropdown = {34.0f, 84.0f, 232.0f, 34.0f};
    if (GuiDropdownBoxRounded(
            dropdown,
            "Rectangle;Circle;Line;Text;Button;Text area;Dropdown;Accordion;Card;Calendar;Switch;Table;Notification;Scrollable area;Image;Web content",
            &p_ui->selected_type,
            p_ui->dropdown_open,
            0.32f,
            10)) {
        p_ui->dropdown_open = !p_ui->dropdown_open;
    }
}