diff infinite_canvas/dev_ui.c @ 276:b55c22cff335

Add interactive infinite canvas prototype
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 16:57:56 -0700
parents
children 8d560f50ed4c
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/infinite_canvas/dev_ui.c	Mon Aug 17 16:57:56 2026 -0700
@@ -0,0 +1,347 @@
+#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, 436.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 ? (Color){220, 38, 38, 255} : 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)
+{
+    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 (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_Clear(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;
+    }
+
+    DrawTextEx(font, TextFormat("%d FPS", GetFPS()), (Vector2){34.0f, 354.0f}, 12.0f, 0.0f, p_theme->text_muted);
+    DrawTextEx(font, TextFormat("%.2fx zoom", p_camera->zoom), (Vector2){104.0f, 354.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, 354.0f},
+        12.0f,
+        0.0f,
+        p_theme->text_muted);
+    DrawTextEx(
+        font,
+        "Drag objects directly on the canvas",
+        (Vector2){34.0f, 400.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.
+    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;
+    }
+}