Mercurial
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; + } +}